Kubernetes 中优雅停止服务的那些事
前言
所谓 "优雅停止服务" 一般指不对线上产生影响,或尽可能减少影响地停止服务产生的影响。
现在高可用服务一般由多实例构成,并且客户端请求由负载均衡器 (Load Balancer) 统一路由。
优雅停止流程大致如下:
- 先通知负载均衡器将该实例从后端列表中移除
- 结束当前实例上连接
- 待当前实例上连接结束或超过限定时间后,停止服务
在实际 Kubernetes 分布式系统中,要做好这件事有许多细节需要注意。本文以 TiDB Server 的优雅停止举例,说明 Kubernetes 中 Pod 删除流程,我们可以做什么,来实现优雅地停止服务。
在使用姿势上,我们并不引入新的流程。而是结合 Kubernetes 提供容器生命周期扩展点实现功能。
从负载均衡器下线
Pod 在标记删除后,Kubernetes 的 Endpoint 控制器会开始将 Pod 的 IP 从 Sevice 后端移除。kube-proxy 以及外部的负载均衡器实现,就会将其从后端移除。
同时 Pod 在标记删除后,kubelet 也会停止容器。这两项操作,由不同组件在发现 Pod 即将删除后操作,并没有先后顺序。若进程在负载均衡器将 Pod IP 从后端移除前,kubelet 就将容器停止,则仍然可能会有新的请求,尝试连接一个不存在的 IP 。若业务层没有重试机制,则请求会失败。
所以,我们需要等待一段时间,再去停止容器。我们可以借助 preStopHook 钩子,在容器停止前等待一段时间:
lifecycle: preStop: exec: command: ["sh", "-c", "sleep 10"]
我们不采用同步方案,让负载均衡器与 kubelet 严格地保证操作顺序执行,因为会过于复杂,且与具体外部负载均衡器实现耦合。例子中的 sleep 时间,可结合生产环境中负载均衡器下线后端反应时间来调整。
结束当前实例上连接
前面我们实现了,先让 Pod 从负载均衡器下线,再结束进程,以避免新的请求在进程结束后,连入进来。但进程还存在当前连接,我们需要结束进程前,先通知进程主动通知客户端关闭连接等。
这步操作与具体的业务的实现有关,我们以 tidb-server 举例。
kubelet 的默认停止容器使用的 SIGTERM 信号,tidb-server 在此信号下会进行优雅退出,但超时时间只有 15 秒,若线上有比较耗时较长的请求,是不够当前连接正常退出的。
tidb-server 在收到 SIGQUIT 信号时会进行不限时的优雅退出,流程如下:
- 关闭 listeners
- 遍历当前连接,关闭空闲连接
- 其余的尝试在应用层协议通知客户端主动关闭
因此,我们不可能使用 kubelet 的默认停止容器使用的 SIGTERM 信号,而是应主动发送 SIGQUIT 给 tidb-server 进程通知其以更优雅的方式运行。
lifecycle: preStop: exec: command: ["sh", "-c", "sleep 10 && kill -QUIT 1"]
PID 1 为容器内 root 进程。注意需要将进行作为 root 进程运行,或者 root 进程可以将信号转发给子业务进程,比如使用 tini 时。
优雅停止超时时间
前面我们不仅在停止服务前通知负载均衡器先将 Pod IP 从后端移除,同时通知应用采用主动通知并结束当前连接后,再退出。但还有一个问题是,许多时候,当前连接要完全优雅结束需要很久,比如一些长连接应用。Kubelet 给 Pod 默认允许的优雅退出时间是 30s,我们需要结合具体应用,配置恰当的超时时间。可以在 pod.spec.terminationGracePeriodSeconds 字段配置:
terminationGracePeriodSeconds: 60 lifecycle: preStop: exec: command: ["sh", "-c", "sleep 10 && kill -QUIT 1"]
至此,我们就实现了完美的优雅退出方案。
结语
我们可以将优雅退出阶段分为以下几部分:
- 通知负载均衡器将 Pod IP 从后端列表下线
- 通知应用优雅结束当前连接
- 配置合理的超时时间,给予应用足够的优雅退出时间
其中具体时间参数,以及通知应用方法需要结合具体应用而定。本文抛砖引玉,以 tidb-server 为例,主要分析思路,和 Kubernetes 中可采用机制。
转发自:https://zhuanlan.zhihu.com/p/188674410