K8s 中微服务优雅滚动更新

status
Published
type
Post
slug
spring-cloud-microservice-graceful-upgrade-in-k8s
date
May 15, 2022
tags
K8s
Java
DevOps
summary
本文介绍了在 Kubernetes 中实现 Spring Cloud 微服务的优雅滚动更新。通过配置 Eureka 和 Ribbon 的参数来缩短服务注册信息的刷新间隔,并让网关在请求异常时重试一次。另外,通过设置 Pod 的 terminationGracePeriod 和 PreStop 钩子来主动下线旧版本的微服务,保证服务的平滑终止。最后,建议集成 Spring Cloud Kubernetes 来代替 Eureka,进一步提升服务升级的平滑度,实现无感知更新。

背景

项目中使用了 Spring Cloud 框架构建微服务,部署在 Kubernetes 集群上。但是由于并没有使用 Spring Cloud Kubernetes,相应的服务注册与服务发现还是通过 Eureka 来完成的。这也导致了如下问题:
在服务发版更新时,尽管通过修改 Deployment 的健康检查等配置依托 K8s 的调度能力实现了滚动更新,但在新版本服务 Pod 启动注册到 Eureka ,停止旧版本服务 Pod 时会出现大约30秒到1分钟的服务不可用时间——访问接口报 500 错误。
于是想要优化一下以期望能达到服务发版更新对用户来说完全无感知的效果。

原因

在 Spring Cloud 架构下,用户请求一般是先到网关,然后由网关按照相应规则转发到相应的微服务中。整个过程简单来说就是:服务更新启动后,先将自身的服务注册信息报告给 Eureka ,然后其他服务会定期访问注册中心获取最新的服务注册列表,进而可以发起调用。然而在使用了 K8s 的滚动更新时,会有如下情况:
假定当前要更新 Service A, 现版本为 v1, 新版本为 v2。
v1 正常运行, v2 启动初始化注册到 Eureka ,健康检查通过,于是 K8s 终止 v1 版本。但是此时网关及其他微服务中缓存的服务列表仍含有 v1 版本的信息, 于是当请求到达时,由于相应的 v1 版本服务已经终止,就会抛出异常报错了。

方案

既然问题是出在 Eureka 的缓存列表上,那么首先直接从 Eureka 的配置入手
Eureka server:
Eureka client:
这样之后,服务列表刷新的时间间隔已经很短,但仍然会存在服务不可用的可能,既然请求是由网关转发过来的,那么就配置网关在请求异常时重试一次,这样即使第一次出错了,那么还可以发起第二次请求尝试其他服务副本。因为当前项目使用的是 Zuul 作为网关,故修改 Zuul 的配置:
以上是通过直接对微服务模块配置来尽可能缩短服务注册信息刷新的间隔来解决。那么既然微服务是在 Kubernetes 集群以 Pod 形式运行的,而微服务的更新实际上也就是新版本 Pod 的启动与旧版本 Pod 的终止。问题场景中是旧版本 Pod 中的微服务没有及时从 Eureka 注册中心中移除,于是可以考虑让旧版本的 Pod 在终止前主动将需要自身从注册中心中移除掉。
💡
简单来说,当 Kubernetes 需要终止一个 Pod 时,它会按照以下步骤进行操作:
  1. 设置 Pod 状态为 Terminating ,并从所有服务的 Endpoints 列表中删除。
  1. 执行 PreStop Hook ,发送命令或 HTTP 请求到 Pod 中。
  1. 向 Pod 中的容器发送 SIGTERM 信号,通知容器即将关闭。
  1. 等待指定的优雅终止宽限期(terminationGracePeriod),通常是30秒,期间 PreStop Hook 和 SIGTERM 信号并行执行。
  1. 如果 Pod 在宽限期内终止完成,则 Pod 被删除。否则,Kubernetes 将发送 SIGKILL 信号,强制终止 Pod 中的容器。
此处可将 terminationGracePeriod 稍微延长至 60s,同时配置 PreStop 钩子来达到容器终止前主动下线自身注册信息。如下使用 Eureka Client 自带的强制下线接口,但要注意基础镜像中需要支持 curl 命令,以便调用该接口。
除了使用 curl 命令,还可以在代码中实现一个端点,用代码实现对应的主动下线逻辑,然后在 PreStop 中 定义 HTTP 该端点调用即可。
上面从微服务本身配置以及 K8s Pod 的生命周期钩子调用优化了应用的滚动更新启动过程,但同时还要求平滑,平滑简单来说也就是要保证原有 Pod 在收到终止信号后,停止接收新请求,同时保证目前已在处理的请求不受影响,等待其全部处理完成后才能真正去停止旧服务。幸运的是 Spring Boot 2.3 版本开始已经支持了此特性,配置如下:
后续考虑集成 Spring Cloud Kubernetes,用 K8s 的服务发现来代替 Eureka,配合健康检查探针实现旧版本服务优雅下线与新版本完全就绪后才接收请求,提升服务升级的平滑度,尽可能的做到无感知更新。
 

2020 - 2024 © HK