优雅退出,业务侧需要做的任务是处理SIGTERM信号
如果 Pod 正在处理大量请求(比如 1000 QPS+)时,因为节点故障或「竞价节点」被回收等原因被重新调度,
可能会观察到在容器被 terminate 的一段时间内出现少量 502/504。
为了搞清楚这个问题,需要先理解清楚 terminate 一个 Pod 的流程:
1 | 1、Pod 被删除,状态置为 Terminating。kube-proxy 更新转发规则,将 Pod 从 service 的 endpoint 列表中摘除掉,新的流量不再转发到该 Pod。 |
注意:1和2 两个工作是异步发生的,所以在未设置 preStop 时,可能会出现「Pod 还在 Service Endpoints 中,但是 SIGTERM 已经被发送给 Pod 导致容器都挂掉」的情况,我们需要考虑到这种状况的发生。
了解了上面的流程后,我们就能分析出两种错误码出现的原因:
- 502:应用程序在收到 SIGTERM 信号后直接终止了运行,导致部分还没有被处理完的请求直接中断,代理层返回 502 表示这种情况
- 504:Service Endpoints 移除不够及时,在 Pod 已经被终止后,仍然有个别请求被路由到了该 Pod,得不到响应导致 504
主进程是服务本身
通常的解决方案是,在 Pod 的 preStop 步骤加一个 15s 的等待时间。
其原理是:在 Pod 处理 terminating 状态的时候,就会被从 Service Endpoints 中移除,也就不会再有新的请求过来了。
在 preStop 等待 15s,基本就能保证所有的请求都在容器死掉之前被处理完成
一个简单的示例如下,它使 Pod 被 Terminate 时,总是在 stop 前先等待 15s,再发送 SIGTERM 信号给容器:
1 | containers: |
更好的解决办法,是直接等待所有 tcp 连接都关闭(需要镜像中有 netstat):
1 | containers: |
主进程不是服务本身, 比如使用shell启动业务进程, 需要做额外配置, 然后再结合preStop进行处理
理想情况下,一个容器只有一个进程,但是在现实场景下很难做到,比如,我会用一个 shell 脚本去管理和启动 Java 进程,业务进程是在 shell 中启动的,就成为了 shell 进程的子进程
系统底层默认会向主进程发送 SIGTERM 信号,而对剩余子进程发送 SIGKILL 信号
1、如果shell启动的是单进程,可以在shell 中启动二进制的命令前面加一个exec,这个命令可以让二进制启动的进程代替shell成为主进程,从而业务进程可以接收到SIGTERM
1 |
|
2、shell启动的是多个进程,则不能用exec来解决了,因为exec只能让一个进程成为主进程。可以使用trap或init系统实现多进程启动传递SIGTERM信号。
trap:
1 |
|
init:
dumb-init 和 tini 都可以作为 init 进程,作为主进程 (PID 1) 在容器中启动,然后它再运行 shell 来执行我们指定的脚本 (shell 作为子进程),shell 中启动的业务进程也成为它的子进程,当它收到信号时会将其传递给所有的子进程,从而也能完美解决 SHELL 无法传递信号问题,并且还有回收僵尸进程的能力
制作包含init系统的业务镜像:
1 | FROM ubuntu:latest |
start.sh:
1 |
|
利用preStop执行pod退出前的操作
例如:查找/user/src/app下是否包含size大于0的且以.heapsnapshot结尾的文件,如果有则将其拷贝到/user/src/app/dump/下 ,然后再把/user/src/app/dump/挂载在宿主机上
必要的时候再结合
terminationGracePeriodSeconds字段加以延长优雅退出的时间
1 | template: |
为了在 Pod 被终止时立即停止循环,使用 & 将循环放入后台进程中。这样,在 preStop 钩子完成后,循环将自动终止。
