我们有一个使用 Gunicorn 在 Kubernetes 上运行的 API 服务,该服务经常返回 502、503 和 504 错误。
开始调试时,我发现了奇怪的问题:日志中没有记录到 SIGTERM
信号的消息,所以我首先检查了 Kubernetes —— 为什么它没有发送这个信号?
那么,看起来是这样的。
我们有一个Kubernetes的Pod。
$ kk get pod NAME READY STATUS RESTARTS AGE fastapi-app-89d8c77bc-8qwl7 1/1 运行中 0 38m
看看它的日志:
$ ktail fastapi-app-59554cddc5-lgj42 ==> 已附加到容器 [fastapi-app-59554cddc5-lgj42: fastapi-app]
干掉它
$ kk delete pod -l app=fastapi-app pod: "fastapi-app-6cb6b46c4b-pffs2" 已被删除
我们在他的日志里看到了什么?什么都没有!
... fastapi-app-6cb6b46c4b-9wqpf:fastapi-app [2024-06-22 11:13:27 +0000] [9] [INFO] 应用启动完成。 ==> 容器已终止 [fastapi-app-6cb6b46c4b-pffs2:fastapi-app] ==> 启动了新的容器 [fastapi-app-6cb6b46c4b-9qtvb:fastapi-app] fastapi-app-6cb6b46c4b-9qtvb:fastapi-app [2024-06-22 11:14:15 +0000] [8] [INFO] 启动了 gunicorn 22.0.0 ... fastapi-app-6cb6b46c4b-9qtvb:fastapi-app [2024-06-22 11:14:16 +0000] [9] [INFO] 应用启动完成。
这儿:
这里是一个正常情况的样子
... fastapi-app-59554cddc5-v7xq9:fastapi-app [2024-06-22 11:09:54 +0000] [8] [INFO] 正在等待应用程序关闭。 fastapi-app-59554cddc5-v7xq9:fastapi-app [2024-06-22 11:09:54 +0000] [8] [INFO] 应用程序已成功关闭。 fastapi-app-59554cddc5-v7xq9:fastapi-app [2024-06-22 11:09:54 +0000] [8] [INFO] 服务器进程 [8] 已完成。 fastapi-app-59554cddc5-v7xq9:fastapi-app [2024-06-22 11:09:54 +0000] [1] [ERROR] 工作进程 (pid:8) 收到 SIGTERM 信号,已被终止! fastapi-app-59554cddc5-v7xq9:fastapi-app [2024-06-22 11:09:54 +0000] [1] [INFO] 正在关闭主进程:Master ==> 容器已终止并退出 [fastapi-app-59554cddc5-v7xq9:fastapi-app]
即 Gunicorn 收到一个 SIGTERM
,并正确地终止了工作。
靠!
咱们看看。
怎么停止一个Pod?
这里有一个非常简短的概述,我在Kubernetes: NGINX/PHP-FPM 平滑关闭 — 消除 502 错误中写得更多。
kubectl delete pod
kubelet
从 API 服务器接收一个终止 Pod 的指令kubelet
向 Pod 中容器里的 PID 1 进程发送 SIGTERM
信号,即容器启动时的第一个进程[terminationGracePeriodSeconds]
指定的时间内停止,则发送 SIGKILL
信号以强制终止换句话说,我们的Gunicorn进程应该接收SIGTERM
信号并记录下来,然后开始停止其工作进程(workers)。
相反,什么也得不到,就死了。
为啥?
`SIGTERM
` 在容器内的
让我们看看这个Pod的容器里有哪些进程。
root@fastapi-app-6cb6b46c4b-9qtvb:/app# ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 2576 948 ? Ss 11:14 0:00 /bin/sh -c gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app root 8 0.0 1.3 31360 27192 ? S 11:14 0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app root 9 0.2 2.4 287668 49208 ? Sl 11:14 0:04 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
我们可以看到这里PID 1是 /bin/sh
进程,它通过 -c
来运行 gunicorn
。
我们现在来在 Pod 中运行命令 strace
,看看它接收到了什么信号:
root@fastapi-app-6cb6b46c4b-9pd7r:~/app# strace -p 1 strace: 进程 1 已附加。 wait4(-1);
运行 kubectl delete pod
-但使用 time
命令来测量执行命令所需的时间:
$ time kk 删除 pod 实例 fastapi-app-6cb6b46c4b-9pd7r 已删除 pod "fastapi-app-6cb6b46c4b-9pd7r" 实际时间 0分32.222秒
32秒倒计时...
strace
里面有什么?
root@fastapi-app-6cb6b46c4b-9pd7r: /app# strace -p 1 strace: 进程 1 已附加, wait4(-1, <未完成...>) = ? ERESTARTSYS (如果设置了 SA_RESTART 选项,则调用会被重新启动) --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=0, si_uid=0} --- wait4(-1, <未完成...>) = ? 命令以退出码 137 结束
这里到底发生了什么?
kubelet
向 PID 1 的进程发送了 SIGTERM
信号 - _SIGTERM {sisigno=SIGTERM} - PID 1 需要将此信号传递给其子进程,并停止它们,最后终止自身kubelet
等待了默认的 30 秒,以确保进程正确结束 - 请参阅 Pod 阶段kubelet
终止了容器,进程以“退出码 137 结束”通常,137 退出码与 OutOfMemory Killer 有关,指的是当一个进程因 SIGKILL
被强制终止时。并没有 OOMKill 的情况,只是因为 Pod 中的进程没有在规定时间内终止,因此发送了 SIGKILL
信号。
我们的SIGTERM
到哪儿去了呢?
直接从容器发出信号——先尝试kill -s 15
,也就是先发送SIGTERM
,如果不行再用kill -s 9
,也就是发送SIGKILL
root@fastapi-app-6cb6b46c4b-r9fnq:/app# kill -s 15 1 root@fastapi-app-6cb6b46c4b-r9fnq:/app# ps aux USER PID %CPU %MEM VSZ RSS TTY STAT 启动时间 TIME 命令 root 1 0.0 0.0 2576 920 ? Ss 12:02 0:00 /bin/sh -c gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app root 7 0.0 1.4 31852 27644 ? S 12:02 0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app ... root@fastapi-app-6cb6b46c4b-r9fnq:/app# kill -s 9 1 root@fastapi-app-6cb6b46c4b-r9fnq:/app# ps aux USER PID %CPU %MEM VSZ RSS TTY STAT 启动时间 TIME 命令 root 1 0.0 0.0 2576 920 ? Ss 12:02 0:00 /bin/sh -c gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app root 7 0.0 1.4 31852 27644 ? S 12:02 0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
什么?怎么?怎么回事?
为什么SIGTERM
信号被忽略了?尤其是SIGKILL
,它应该被认为是一个“不可忽略的信号”——参阅man signal。
_> 信号 SIGKILL 和 SIGSTOP 这两个不能被捕获、阻止或被忽略。
kill()
函数 以及 PID 1
因为 Linux 中的 PID 1 是一个特殊的进程,它是由系统最先启动的,必须避免意外被“杀死”。
如果我们看看《kill》手册,其中明确说明了,并且还提到了进程中的信号处理程序。
_> 只有 init 进程(PID 1)明确定义了信号处理程序的那些信号才能发送给进程 ID 1。这可以防止系统因意外而被关闭。
你可以从文件 /proc/1/status
中查看程序可以拦截并处理哪些信号:
root@fastapi-app-6cb6b46c4b-r9fnq:/app# cat /proc/1/status | grep SigCgt SigCgt: 0000000000010002
The SigCgt
信号是进程可以自行拦截并处理的信号。其余信号要么被忽略,要么按照 SIG_DFL
处理。PID 1 没有自己的处理程序,因此会忽略这些信号。PID 1 接收的信号会被 SIG_DFL
处理忽略。
咱们问一下ChatGPT这些信号到底是什么:
(如果你感兴趣,可以自己试着翻译一下 — 例如如何查看一个进程监听了哪些信号?,或者如何解读信号的位掩码)
所以来看看有什么:
/bin/sh
的PID是1SIGHUP
信号和 SIGCHLD
信号SIGTERM
信号和 KILL信号
SIGTERM
信号和 KILL信号
但是容器又是怎么停下来呢?
停止 Docker 或 Containerd 中的容器的过程与在 Kubernetes 中停止容器的过程并无不同,因为实际上,kubelet
只是向容器运行时发送命令。在 AWS Kubernetes 中,现在用的是 containerd
。
但为了简便起见,我们用本地Docker来做。
我们从我们在Kubernetes中测试过的同一个Docker镜像启动容器。
$ docker run --name test-app 492***148.dkr.ecr.us-east-1.amazonaws.com/fastapi-app-test:entry-2 [2024-06-22 14:15:03 +0000] [7] [INFO] 启动了 gunicorn 22.0.0 [2024-06-22 14:15:03 +0000] [7] [INFO] 监听在: http://0.0.0.0:80 (7) [2024-06-22 14:15:03 +0000] [7] [INFO] 使用工作进程类型: uvicorn.workers.UvicornWorker [2024-06-22 14:15:03 +0000] [8] [INFO] 启动了工作进程,进程ID:8 [2024-06-22 14:15:03 +0000] [8] [INFO] 启动了服务器进程 [8] [2024-06-22 14:15:03 +0000] [8] [INFO] 等待着应用启动。 [2024-06-22 14:15:03 +0000] [8] [INFO] 应用已经启动完成。
尝试通过向PID 1发送SIGKILL
信号来停止它,但没有任何效果,它忽略了该信号。
$ docker exec -ti test-app sh -c "kill -9 1" # 终端执行命令,强制终止容器内的进程1 $ docker ps # 查看当前运行的容器 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 99bae6d55be2 492***148.dkr.ecr.us-east-1.amazonaws.com/fastapi-app-test:entry-2 "/bin/sh -c 'gunicorn --workers 3 --bind 0.0.0.0:8080 app:app'" 大约一分钟前 大约一分钟前启动 test-app # 启动应用程序,指定3个工作进程并绑定到0.0.0.0:8080端口
尝试使用 docker stop
命令停止它,再看一下时间。
$ time docker stop test-app test-app 用时: 0m10.234s
容器的状态:
$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES cab29916f6ba 492***148.dkr.ecr.us-east-1.amazonaws.com/fastapi-app-test:entry-2 "/bin/sh -c 'gunicorn…" 大约一分钟前 52秒前退出(137)
注:退出(137)表示容器异常终止。
代码 137 代表容器通过 SIGKILL
信号停止,容器停止用了 10 秒。
但如果信号发送到了PID 1,它却忽略了呢?
但我们可以通过两种方式结束容器,这在 docker kill
的文档里没有提到。
我们再来看看容器里的情况:
root@cddcaa561e1d:/app# ps aux 用户 PID %CPU %MEM VSZ RSS TTY 状态(STAT) 启动时间 运行时间 命令 root 1 0.0 0.0 2576 1408 ? Ss 15:58 0:00 /bin/sh -c gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app root 7 0.1 0.0 31356 26388 ? S 15:58 0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app root 8 0.5 0.1 59628 47452 ? S 15:58 0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app root@cddcaa561e1d:/app# pstree -a sh -c gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app └─gunicorn /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app └─gunicorn /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
我们不能杀死PID 1,因为它无视我们,但我们能搞定PID 7!
然后它会杀死PID 8,因为它自己的子进程,当PID 1发现没有子进程时,它自己也会死亡,容器就会停止运作:
如下是在Linux命令行中输入的命令:```
root@cddcaa561e1d:/app# kill 7
运行此命令是为了结束进程号为7的程序。 容器的日志也包括在内:容器日志
... [2024-06-22 16:02:54 +0000] [7] [INFO] 处理信号:TERM [2024-06-22 16:02:54 +0000] [8] [INFO] 关闭中 [2024-06-22 16:02:54 +0000] [8] [INFO] 关闭套接字时出错 [Errno 9] 文件描述符无效 [2024-06-22 16:02:54 +0000] [8] [INFO] 等待应用程序关闭。 [2024-06-22 16:02:54 +0000] [8] [INFO] 应用程序关闭完成。 [2024-06-22 16:02:54 +0000] [8] [INFO] 服务器进程 [8] 已完成 [2024-06-22 16:02:54 +0000] [7] [ERROR] 工作进程 (pid:8) 收到了 SIGTERM 信号! [2024-06-22 16:02:54 +0000] [7] [INFO] 关闭:主进程
但是因为Pods/容器的退出码是137,这意味着它们被`SIGKILL`信号强制终止,因为当Docker或其他容器运行工具无法用`SIGKILL`信号停止容器中的PID 1进程时,它会向容器内的所有进程发送`SIGKILL`信号。 也就是说: 1. 首先,向PID 1发送`SIGTERM`信号 2. 10秒后,向PID 1发送`SIGKILL`信号 3. 如果没有效果,则向容器内的所有进程发送`SIGKILL`信号 比如说,你可以通过将 SID 传给 `kill` 命令来做到这一点。 找到容器中的主要进程:
$ docker 检查 --format '{{ .State.Pid }}' test-app 查看容器test-app的PID 629353
在终端中输入以下命令:`ps j -A` :
$ ps j -A
PPID PID PGID SID TTY TPGID STAT UID 时间 命令
……
629333 629353 629353 629353 ? -1 Ss 0 0:00 /bin/sh -c gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app (gunicorn 启动命令)
629353 629374 629353 629353 ? -1 S 0 0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app (gunicorn 启动命令)
629374 629375 629353 629353 ? -1 S 0 0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app (gunicorn 启动命令)
我们看到我们的SID — _629353_。 把整个小组全杀了:
执行命令:sudo kill -9 -- -629353
$ sudo kill -9 -- -629353 好吧。 这一切都非常棒和极其有趣。 但是,我们是不是可以不用这些拐棍呢? 启动容器中的进程的正确方法 最后来看看我们的 `Dockerfile`:
FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . ENTRYPOINT gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
请参阅[Docker — Shell 和 exec 形式文档](https://docs.docker.com/reference/dockerfile/#shell-and-exec-form)页面: _> _指令 ["可执行文件”,"参数1", "参数2"] (执行形式) > 命令 command 参数1 参数2 (shell形式) 因此,结果是 `/bin/sh` 作为 PID 1 进程,通过 `- c` 选项启动了 Gunicorn。 如果我们用 _exec形式(或直接用程序术语,如 `_exec` 格式`_`)_ 写它:
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENTRYPOINT ["gunicorn", "-w", "1", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:80", "app:app"]
然后我们运行一个基于这个镜像的容器 (container),我们只会有这些 Gunicorn 进程:
root@e6087d52350d:/app# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.6 0.0 31852 27104 ? Ss 16:13 0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
root 7 2.4 0.1 59636 47556 ? S 16:13 0:00 /usr/local/bin/python /usr/local/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:80 app:app
已经可以处理 `SIGTERM` 信号的哪个:
root用户在容器e6087d52350d的/app目录下# cat /proc/1/status | grep SigCgt
SigCgt: 0000000008314a07
![](https://imgapi.imooc.com/6705eb4109464fb407760301.jpg) 现在,如果我们向 PID 1 发送终止信号 `SIGTERM`,容器将会正常退出。
root@e6087d52350d:/app# kill 1
``` 这条命令会终止进程ID为1的进程。
还有日志:
[2024-06-22 16:17:20 +0000] [1] [INFO] 处理信号:终止信号 [2024-06-22 16:17:20 +0000] [7] [INFO] 正在关闭 [2024-06-22 16:17:20 +0000] [7] [INFO] 关闭套接字时出错 [Errno 9] 无效的文件句柄 [2024-06-22 16:17:20 +0000] [7] [INFO] 等待应用程序关闭完成 [2024-06-22 16:17:20 +0000] [7] [INFO] 应用程序关闭完成 [2024-06-22 16:17:20 +0000] [7] [INFO] 服务器进程 [7] 已完成 [2024-06-22 16:17:20 +0000] [1] [ERROR] 工作进程 (pid:7) 收到 SIGTERM 信号! [2024-06-22 16:17:21 +0000] [1] [INFO] 主进程正在关闭
现在 Kubernetes 命名空间中的 Pod 将会正常地停止服务——而且会非常快,因为不会等待宽限期,这样一来。
SIGKILL
信号如何处理 PID 1 进程杀死进程及其所有子进程
最初发布于 RTFM:Linux、DevOps 和系统运维 .