容器技术回顾 - 什么是优雅关闭以及如何实现


容器即进程,那就首先我们来回顾一下如何使用 kill 命令来优雅关闭一个进程。当我们使用 kill 命令给一个进程或者一组进程发送信号的时候,可以采用多种信号,那就让我们先来回顾一下 Kill 命令吧。
Kill 命令
概要
kill [-s sigspec | -n signum | -sigspec] pid | jobspec ...kill -l [sigspec]
主要用途
发送信号到作业或进程(可以为多个)。
选项
-s sig 信号名称。-n sig 信号名称对应的数字。-l 列出信号名称。如果在该选项后提供了数字那么假设它是信号名称对应的数字。-L 等价于-l选项。
参数
pid:进程ID
jobspec:作业标识符
返回值
返回状态为成功除非给出了非法选项、执行出现错误。
其中信号包括:
kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR111) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+338) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+843) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+1348) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-1253) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-758) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-263) SIGRTMAX-1 64) SIGRTMAX
常用信号
# 只有第 9 种信号(SIGKILL)才可以无条件终止进程,其他信号进程都有权利忽略。HUP 1 终端挂断INT 2 中断(同 Ctrl + C)QUIT 3 退出(同 Ctrl + \)KILL 9 强制终止TERM 15 终止CONT 18 继续(与STOP相反,fg/bg命令)STOP 19 暂停(同 Ctrl + Z)
进程的优雅关闭实际上就是指:
我们发送 SIGTERM(15)信号给到程序(进程)
程序(进程)收到 SIGTERM 信号后做清理工作,包括给子进程发送 SIGTERM 信号并等待退出
最终程序(进程)完成清理工作,调用 exit(0)实现优雅退出(退出码也可以是 143,代表优雅退出)。
容器的优雅关闭
在容器技术回顾  - 容器中的 0 号进程和 1 号进程一文中,我们知道:
Docker 提供了两个命令 docker stop 和 docker kill 来向容器中的 1 号进程发送终止信号:
docker stop:docker 会首先向容器的 PID 1 进程发送一个 SIGTERM 信号,用于容器内程序的退出。如果容器在收到 SIGTERM 后没有结束, 那么 Docker Daemon 会在等待一段时间(默认是10s)后,再向容器发送 SIGKILL 信号,将容器杀死变为退出状态。也就是说如果我在 1 号进程实现了 SIGTERM(15) 信号处理,就实现了优雅停止。
docker kill:可以向容器内 PID 1 进程发送任何信号,缺省是发送 SIGKILL 信号来强制退出应用(这里在宿主机上对 1 号进程下发的 kill)。
也就是说,如果容器的 1 号进程实现了对于 SIGTERM 信号的处理,那么容器就能实现优雅关闭。Java 程序,尤其是 Spring 应用,都实现了对于 SIGTERM 信号的处理。而 bash 进程通常情况下没有实现,如果需要实现优雅关闭,需要实现类似以下逻辑:
#!/bin/bash # Function to handle SIGTERMcleanup() { echo "收到 SIGTERM 信号。向 Java 进程发送 SIGTERM 信号..." # 向 Java 进程发送 SIGTERM 信号 [ -n "$java_pid" ] && kill -15 "$java_pid" # 如果需要,可以添加其他清理逻辑 echo "清理完成。退出脚本。" exit 0} # Register the cleanup function for SIGTERMtrap 'cleanup' SIGTERM echo "开始运行 Java 应用程序!" # 在后台运行 Java 应用程序java -jar test-demo-0.0.1-SNAPSHOT.jar & # 保存 Java 进程的 PIDjava_pid=$! # 等待 Java 进程完成wait "$java_pid"
而我们很多容器的启动脚本都是需要 shell 来启动业务进程(例如 Java 进程),除了上述方式之外,有什么方法吗?这个时候我们就需要回顾一下 Dockerfile 的 CMD 和 Entrypoint 了:
CMD/Entrypoint
Docker ENTRYPOINT 和 CMD 可以有两种形式,即 Shell 和 Exec 形式。例如:
<指令> <命令> ---> shell 形式
<指令> [“可执行文件”,“参数”] ---> exec 形式
CMD echo “Hello World”(shell形式)CMD [“echo”,“Hello World”]( exec 形式)ENTRYPOINT echo “Hello World”(shell 形式)ENTRYPOINT [“echo”,“Hello World”](exec 形式)
Linux系统 - 进程管理入门一文中我们知道了 fork 和 exec 的差别:
fork 会创建一个新的进程(父子两个进程)
exec 会加载程序覆盖原程序(只有一个进程)
因此我们使用 exec 方式,这样就不会新启动一个进程来执行命令(例如 bash),这样我们的业务进程可以变为 1 号进程,这样就可以接收到 SIGTERM 信号了,从而实现优雅关闭。
容器实现优雅关闭的几种方式
Dockerfile 里面使用 exec 方式让多进程管理程序(例如 tini)成为 1 号进程,由它管理其他进程;
Dockerfile 里面使用 exec 执行 Java 进程;
Dockerfile 里面使用 exec 执行一个 bash 脚本,bash 脚本实现:
SIGTERM 信号处理
或者 exec 方式执行 Java 进程,让 Java 进程替换 bash 进程变为 1 号进程;
另外 shell 脚本要调用另外的 shell 脚本,请使用 source 命令,让其他脚本在当前 shell 进程中执行。
示例
启动 Java 的 shell 脚本 boot.sh
#!/bin/bash echo "Start to run java app!"## 这里有一堆的初始化工作#exec java -jar test-demo-0.0.1-SNAPSHOT.jar echo "End!" #此语句不会被执行,因为 exec 之后 java 进程就会替换当前的 bash 进程
Dockerfile
FROM ........ENTRYPOINT ["/bin/bash","-c", "/home/app/boot.sh"]
Kubernetes 的优雅关闭
当 Kubernetes 杀死一个 pod 时,会发生以下 5 个步骤:
1、 Pod 切换到终止状态并停止接收任何新流量,容器仍在 pod 内运行。
2、 preStop 如果被定义,则相应的方法将被执行。
3、 kubelet 对 Pod 中各个 container 发送调用 cri 接口中 StopContainer 方法,例如向 dockerd 发送 stop -t 指令,用 SIGTERM 信号以通知容器内应用进程开始优雅停止。
4、 Kubelet 等待一段时间(terminationGracePeriodSeconds,默认 30 秒)。此等待与 preStop hook 和 SIGTERM 信号并行执行。如果等待时间结束,则直接进入下一步。
5、如果 Pod 的容器在宽限期后仍在运行,Kubelet 则会向 Pod 的仍在运行容器的 1 号进程发送 SIGKILL 信号,这样所有仍在运行的容器会被强制杀掉,Pod 就会停止,最后被移除。

Pod Stop 流程
preStop 可以做一些清理工作,例如发 SIGTERM 信号给 1 号进程的子进程。
总结
容器要实现优雅关闭,必须要满足以下条件:
容器的 1 号进程必须能够处理 SIGTERM 信号,如果有子进程,则需要让子进程也优雅关闭;
bash 脚本默认不具备给子进程发送优雅关闭信号的能力,因此需要自己写代码实现;
可以使用 exec 方式让子进程替换当前进程,这样原先有两个父子进程,现在就会变为一个进程;
Dockerfile 的 CMD 和 Entrypoint 支持 exec 方式来启动进程,我们应该尽量使用这种方式。
到顶部