为了账号安全,请及时绑定邮箱和手机立即绑定

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] 应用启动完成。

这儿:

  1. 服务已开始运行 — “Application startup complete
  2. Pod 容器退出了 — “Container left
  3. 新的 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 错误中写得更多。

  1. 我们执行 kubectl delete pod
  2. 对应的 WorkerNode 上的 kubelet 从 API 服务器接收一个终止 Pod 的指令
  3. kubelet 向 Pod 中容器里的 PID 1 进程发送 SIGTERM 信号,即容器启动时的第一个进程
  4. 如果容器没有在 [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 结束

这里到底发生了什么?

  1. kubelet 向 PID 1 的进程发送了 SIGTERM 信号 - _SIGTERM {sisigno=SIGTERM} - PID 1 需要将此信号传递给其子进程,并停止它们,最后终止自身
  2. 但是该进程没有停止——kubelet 等待了默认的 30 秒,以确保进程正确结束 - 请参阅 Pod 阶段
  3. 然后 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 的文档里没有提到。

  1. 杀死容器内的所有子进程——然后父进程(PID 1)也会随之死亡
  2. 通过它们的 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

复制requirements.txt到工作目录

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 将会正常地停止服务——而且会非常快,因为不会等待宽限期,这样一来。

คู่มือ有用的链接
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消