Kubernetes:容器技术和所谓的“丢失”的SIGTERM信号(终止信号)
我们有一个使用 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] 应用启动完成。
这儿:
- 服务已开始运行 — “Application startup complete”
- Pod 容器退出了 — “Container left”
- 新的 Pod 启动了 — “新容器” 和 “启动 gunicorn”
这里是一个正常情况的样子
...
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
,并正确地终止了工作。
靠!
咱们看看。
Kubernetes 和 Pod 的终止流程怎么停止一个Pod?
这里有一个非常简短的概述,我在Kubernetes: NGINX/PHP-FPM 平滑关闭 — 消除 502 错误中写得更多。
- 我们执行
kubectl delete pod
- 对应的 WorkerNode 上的
kubelet
从 API 服务器接收一个终止 Pod 的指令 kubelet
向 Pod 中容器里的 PID 1 进程发送SIGTERM
信号,即容器启动时的第一个进程- 如果容器没有在
[terminationGracePeriodSeconds]
指定的时间内停止,则发送SIGKILL
信号以强制终止
换句话说,我们的Gunicorn进程应该接收SIGTERM
信号并记录下来,然后开始停止其工作进程(workers)。
相反,什么也得不到,就死了。
为啥?
进程ID 1 以及`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 这两个不能被捕获、阻止或被忽略。
Linux 的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是1 - PID 1 是一个特殊的进程
- 检查PID 1会发现它只“识别”
SIGHUP
信号和SIGCHLD
信号 - 并且它会忽略
SIGTERM
信号和KILL信号
- 并且它会忽略
SIGTERM
信号和KILL信号
但是容器又是怎么停下来呢?
Docker 停止与 Linux 信号处理停止 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
的文档里没有提到。
- 杀死容器内的所有子进程——然后父进程(PID 1)也会随之死亡
- 通过它们的 SID(会话 ID)杀死主机上的整个进程族——这将导致 PID 1 不再接收信号,因此因其所有子进程都已死亡而自身死亡
我们再来看看容器里的情况:
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` 格式`_`)_ 写它:
这是一个Dockerfile,用于构建Python 3.9的web应用程序
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 将会正常地停止服务——而且会非常快,因为不会等待宽限期,这样一来。
คู่มือ有用的链接- 为什么有时容器中的 PID 1 进程无法被杀死 — 一篇精彩的文章,解释了 Linux 内核中
SIGKILL
信号如何处理 PID 1 进程 - 内核中的信号如何工作 — 以及更多关于内核中信号如何工作的信息
- 为什么你需要在 Docker 容器中有一个 init 进程 (PID 1)
-
最初发布于 RTFM:Linux、DevOps 和系统运维 .
共同学习,写下你的评论
评论加载中...
作者其他优质文章