抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

问题背景

Kubernetes使用Deployment来控制Pod的主要好处之一是能够执行滚动更新。
滚动更新允许你逐步更新Pod的配置,并且Deployment提供了许多选项来控制滚动更新的过程。
很多时候滚动更新过程,服务会很出现短暂的停机现象,期间访问服务会失败。
那么,如何做到滚动更新过程零停机?

原因分析

在给出方案之前,先看看为何会发生服务中断。

Deployment 滚动更新时会先创建新 pod,等待新 pod running 后再删除旧 pod。

  • Pod 创建后进入 running 状态会被加入到 Endpoint 后端,默认情况下会直接进入 Ready 状态,开始接收流量。
  • 在删除旧 pod 过程中需要对多个对象(如 Endpoint、ipvs /iptables、SLB)进行状态同步,并且这些同步操作是异步执行的,整体同步流程如下图所示。

pod 状态变更:
1.将 Pod 设置为 Terminating 状态,并从所有 Service 的 Endpoints 列表中删除。此时,Pod 停止获得新的流量,但在 Pod 中运行的容器不会受到影响;
2.执行 preStop Hook:Pod 删除时会触发 preStop HookpreStop Hook 支持 bash 脚本、TCP 或 HTTP 请求;
3.发送 SIGTERM 信号:向 Pod 中的容器发送 SIGTERM 信号;
4.等待指定的时间:terminationGracePeriodSeconds 字段用于控制等待时间,默认值为 30 秒。该步骤与 preStop Hook 同时执行,因此 terminationGracePeriodSeconds 需要大于 preStop 的时间,否则- 会出现 preStop 未执行完毕,pod 就被 kill 的情况;
5.发送 SIGKILL 信号:等待指定时间后,向 pod 中的容器发送 SIGKILL 信号,删除 pod。

简单说,服务中断原因在于:

  • 创建Pod时,Pod 业务代码可能还未初始化完毕就开始接收流量,此时请求,服务会中断;
  • 删除Pod时,上面1~4步骤同时进行,所以 Pod 收到 SIGTERM 信号并且停止工作后,可能还未从 Endpoints 中移除,此时请求,服务也会中断。

解决思路

1.配置滚动更新策略,保证始终有处于 Ready 状态的 pod 来提供服务。
2.使用 preStop 钩子,延迟 pod 的删除,给新创建的 pod 提供预热时间。
3.使用就绪探针,检查我们的应用程序是否已经准备好来处理流量了。

测试验证

工具:使用ab工具模拟持续请求,1000并发,重复10000次。

方法:在压测过程手动编辑部署实现重启,触发滚动更新。

说明:请求失败时每次运行结果都会有差异,下面是在多次运行中随机取一次结果。

现象:

等级1: 默认策略

如果你什么都没配置,k8s会有默认策略:滚动过程最大可超过预期副本25%,最大不可用副本数量为25%。

1
2
3
4
5
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 25%
maxSurge: 25%

单个副本下:

两个副本下:

等级2: 设置滚动更新策略

设置策略为滚动更新,且滚动过程最大可超过预期副本1个,最大不可用副本数量为0个,即保证至少有一个副本是Ready。

1
2
3
4
5
6
7
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0

单个副本下:

两个副本下:

等级3: 设置pod生命周期钩子

我们这里使用 preStop 设置了一个 8s 的宽限期 (推荐5~10s),Pod 在真正销毁前会先 sleep 等待 8s,这就相当于留了时间给 Endpoints 控制器和 kube-proxy 更新去 Endpoints 对象和转发规则。
这段时间 Pod 虽然处于 Terminating 状态,即便在转发规则更新完全之前有请求被转发到这个 Terminating 的 Pod,依然可以被正常处理,因为它还在 sleep,没有被真正销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spec:
template:
spec:
containers:
- lifecycle:
preStop:
exec:
command: ["/bin/bash", "-c", "sleep 8"]
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1

单个副本下:

两个副本下:

等级4: 设置就绪探针

设置http就绪探针,需要应用代码支持,返回code=200~400则会被视为就绪,其他都会视为异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spec:
template:
spec:
containers:
- lifecycle:
preStop:
exec:
command: ["/bin/bash", "-c", "sleep 8"]
readinessProbe:
httpGet:
path: /healthz
port: 7001
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1

单个副本下:

两个副本下:

实践结论

滚动更新是k8s的一大特性,可以控制pod升级行为,目的是减少更新过程中出现服务中断的现象。
但滚动更新不等于不会停机,若想实现零停机滚动更新,需要通过各种手段来阻止流量打到被不可用的pod上的情况,任何一个漏洞的存在都可能出现断流现象
因此,如何把这些漏洞“堵住”是一个技术活,针对不同项目有不同的配置,比如就绪探针和preStop钩子,需要针对项目实际情况来设置。

通常,有以下几种配置手段:
1、设置滚动更新策略,让应用始终保留有 Ready 状态的pod副本,但还是存在pod被删除,但Endpoint还没刷新,导致流量打到被终止的pod上造成断流。
2、设置proStop钩子,延迟pod的终止行为,腾出足够时间让控制器将其从Service删除,实现摘流(即要在Pod真正停止运行前,先把它从Service上拿掉),但流量还是可能进入未就绪但状态为Ready的pod上造成断流。
3、设置就绪探针,保证应用启动后能正常提供服务再变成Ready状态,避免pod被创建时的断流现象。

总的来说,滚动更新是一个服务升级方式,但具体策略需要我们控制好才能规避更新过程出现服务中断现象。

好文推荐

使用 PDB 避免 Kubernetes 集群中断

借助 Pod 删除事件的传播实现 Pod 摘流

如何优雅地关闭Kubernetes集群中的Pod

Kubernetes群集的零停机服务器更新

Kubernetes–玩转Pod滚动更新123

浅析Kubernetes Pod重启策略和健康检查

更新应用时,如何实现 K8s 零中断滚动更新

评论