概述
在删除pod时,容器中的应用进程可能还在处理请求,需要保证已存在的连接不断开,新的连接不过来,最后再停止进程。这个过程就叫做优雅终止。
删除pod的过程
- 用户提交删除pod请求,将pod状态设置为Terminating。
- kube-proxy更新iptables,将pod从service的endpoint列表中摘除掉,新的流量不再转发到该pod。
- 如果pod配置preStop Hook,执行该Hook。
- kubelet对pod中所有container发送SIGTERM信号(kill -15/kill)停止应用进程。
- 等待容器进程完全停止,如果在terminationGracePeriodSeconds内 (默认 30s) 还未完全停止,就发送SIGKILL信号 (kill -9) 强制杀死进程。
- 所有容器进程终止,清理pod资源。
SIGTERM信号收不到
应用必须要有处理SIGTERM信号的能力,这样才能实现优雅终止。在实现的过程中可能会有一个坑:容器进程收不到SIGTERM信号。
如果容器的EntryPoint使用了脚本,业务进程就成了shell的子进程,在pod停止时业务进程可能收不到SIGTERM信号,因为 shell 不会自动传递信号给子进程。
栗子
1.直接定义命令
这个栗子是Dockerfile中定义了ping localhost
,但是实际上容器内该pid为1的是一个shell进程,ping命令成为了shell的子进程。这样就会导致外部发送任何信号也无法传递到ping命令,ctrl+c也不行。
cat Dockerfile
FROM centos
CMD ping localhost
docker exec 26ad ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 20:14 ? 00:00:00 /bin/sh -c ping localhost
root 7 1 0 20:14 ? 00:00:00 ping localhost
root 20 0 0 20:15 ? 00:00:00 ps -f
2.使用脚本
cat Dockerfile
FROM centos
ENTRYPOINT ["/start.sh"]
cat start.sh
#! /bin/bash
ping localhost
解决
上面的栗子1是可以修改Dockerfile,使用Exec表示法。
cat Dockerfile
FROM centos
CMD ["/bin/ping localhost"]
但是栗子2已经是Exec表示法了,这时可以有两种方法:
- 尽量不使用shell启动业务进程。
- 如果一定要通过shell启动进程,那么需要在shell中添加处理SIGTERM信号的能力。
shell处理SIGTERM信号
使用exec启动
在shell中启动二进制的命令前加一个exec即可让该二进制启动的进程代替当前shell进程,即让新启动的进程成为主进程:
#! /bin/bash
...
exec /bin/yourapp # 脚本中执行二进制
使用trap
如果容器内需要启动多个进程,那么就不能用exec了,因为exec只能让一个进程成为主进程。这时可以使用trap来捕获信号,当收到信号后触发回调函数来将信号通过kill传递给业务进程,脚本示例:
#! /bin/bash
/bin/app1 & pid1="$!" # 启动第一个业务进程并记录 pid
echo "app1 started with pid $pid1"
/bin/app2 & pid2="$!" # 启动第二个业务进程并记录 pid
echo "app2 started with pid $pid2"
handle_sigterm() {
echo "[INFO] Received SIGTERM"
kill -SIGTERM $pid1 $pid2 # 传递 SIGTERM 给业务进程
wait $pid1 $pid2 # 等待所有业务进程完全终止
}
trap handle_sigterm SIGTERM # 捕获 SIGTERM 信号并回调 handle_sigterm 函数
wait # 等待回调执行完,主进程再退出
使用适当的init进程
tini:https://github.com/krallin/tini
tini作为主进程 (PID 1) 在容器中启动,然后它再运行shell启动脚本,shell作为子进程,shell中启动的业务进程也成为它的子进程,当它收到信号时会将其传递给所有的子进程,从而也能完美解决 SHELL 无法传递信号问题,还可以负责清理异常退出的子进程。Dockerfile栗子如下:
FROM centos
# Add Tini
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]
# Run your program under Tini
CMD ["/your/program", "-and", "-its", "arguments"]
# 或者
ADD start.sh /
ADD app1 /bin/app1
ADD app2 /bin/app2
CMD ["/start.sh"]
cat start.sh
#! /bin/bash
/bin/app1 &
/bin/app2 &
wait
preStop Hook
k8s调用hook有2种方式:exec,http,tcpSocket(暂不支持)。
k explain deployment.spec.template.spec.containers.lifecycle.preStop
其中httpGet和tcpSocket在kubelet进程执行,而exec则由容器内执行 。
如果业务代码中没有处理SIGTERM信号的能力,也无法使用第三方库或系统来增加优雅终止的逻辑,那么可以在pod中使用preStop Hook来实现优雅终止
k explain deployment.spec.template.spec.containers.lifecycle
示例:
lifecycle:
preStop:
exec:
command:
- /clean.sh
如果PreStop失败了,会产生一个FailedPreStopHook 事件,在describe pod的Events中可以看到。
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
...
Warning FailedPostStartHook 4s (x2 over 5s) kubelet Exec lifecycle hook ([badcommand]) for Container "lifecycle-demo-container" in Pod "lifecycle-demo_default(30229739-9651-4e5a-9a32-a8f1688862db)" failed - error: command 'badcommand' exited with 126: , message: "OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: \"badcommand\": executable file not found in $PATH: unknown\r\n"
Normal Killing 4s (x2 over 5s) kubelet FailedPostStartHook
...
在某些极端情况下,pod被删除的一小段时间内,仍然可能有新连接被转发过来,因为kubelet与kube-proxy同时watch到pod 被删除,kubelet有可能在kube-proxy同步完iptables前就已经停止容器了,这时可能导致一些新的连接被转发到正在删除的 pod。
而通常情况下,当应用收到SIGTERM信号后都不再接受新连接,只保持存量连接继续处理,所以就可能导致pod删除的瞬间部分请求失败。这时可以在preStop中使用sleep命令,等待kube-proxy处理完iptables。
lifecycle:
preStop:
exec:
command:
- sleep
- 5s
- 调整优雅终止时长
k explain deployment.spec.template.spec.terminationGracePeriodSeconds
可以看到默认是30s,你的应用(preStop + 业务进程停止)可能超过30s,那么容器就会被kill掉。根据你的业务自定义优雅终止时长比如调整为60s。
An intriguing discussion is worth comment. I do think that you should write more on this topic, it might not be a taboo subject but typically folks dont discuss these issues. To the next! All the best!!