容器是怎么死的
一句话总结
docker stop 会发送 SIGTERM,默认等待 10 秒后再发送 SIGKILL。退出码 143 表示 SIGTERM(128+15),137 表示 SIGKILL(128+9)。只要能区分这两个数字,就能把故障原因缩小一半。
为什么需要了解这一点
曾经有一个服务,每次部署时停止过程都要花费 10 秒,退出码也总是 137。对同一个应用只改变命令形式后重新测量,结果如下。
CMD npm start -> real 0m10.412s, ExitCode 137
CMD ["node","server.js"] -> real 0m0.284s, ExitCode 143
这 10 秒是 Docker 的默认宽限时间,137 表示强制终止。如果是正常终止,进程应该立即结束,并返回 143。
工作原理
原因分为两层。
第一,内核会特殊对待 PID 1。 对于没有显式注册处理器的信号,不会应用默认行为。 Shell 形式的 CMD 实际会以 /bin/sh -c "npm start" 运行,使 Shell 成为 PID 1。Shell 没有 SIGTERM 处理器,因此信号就这样被忽略。
第二,“Shell 不会转发信号”这个解释只说对了一半。 dash 或 busybox ash 在 sh -c 只包含一条简单命令时,可能进行不 fork、直接以 exec 替换自身的优化。这样 Shell 会消失,目标程序有时会成为 PID 1。问题在于,这种优化是有条件的。只要加入管道、&&、变量展开或重定向中的任意一项,Shell 就会保留下来。依赖 Shell 的这种行为,迟早会在不知不觉中失效。
正确答案是使用 JSON 数组形式的 exec 格式;如果使用入口脚本,最后一行必须是 exec "$@"。
实际工作中的表现
PID 1 还有第二项责任。失去父进程的进程会被 PID 1 收养,而 PID 1 必须回收它们的退出状态。init 系统会做这件事,但 Node 或 Python 进程不会。实际案例中,僵尸进程曾累积到 1847 个;进程表填满后,应用日志会出现 fork: Resource temporarily unavailable。
此外,137 也可能是 OOM Killer 造成的。区分两者需要查看 State.OOMKilled 字段和内核日志。应用不会收到任何关于 SIGKILL 的异常,因此,如果容器毫无提示地反复重启,应查看内核日志,而不是应用日志。
信号无法到达 PID 1 的两种情况
停止容器时,运行时会向 PID 1 发送 SIGTERM;如果经过 --stop-timeout(默认 10 秒)后仍存活,就发送 SIGKILL。但有两种情况下,信号无法到达应用。
Shell 形式 CMD。 使用 CMD npm start 时,/bin/sh -c "npm start" 会成为 PID 1,而 Shell 不会把信号转发给子进程。
CMD npm start # ❌ sh 가 PID 1. SIGTERM 을 삼킨다
CMD ["npm", "start"] # ✅ exec 형식 — 애플리케이션이 PID 1
包装脚本。 如果入口脚本最后调用程序时没有使用 exec,脚本会继续作为 PID 1 留下来。
#!/bin/sh
설정_준비
exec "$@" # ← exec 이 있어야 프로세스가 교체되어 PID 1 이 된다
两种情况的症状相同:进程不响应停止请求,并在 10 秒后被强制终止。 日志中不会留下任何内容,正在处理的请求和缓冲区数据都会消失。
PID 1 的另一项义务
在 Linux 中,PID 1 还负责回收孤儿进程。如果应用不做这件事,就会累积僵尸进程。对于会创建子进程的容器(Shell 脚本、构建工具),这一问题尤其常见。
docker run --init ... # tini 를 PID 1 로 넣어 준다
在 Kubernetes 中,使用 shareProcessNamespace: true 时,pause 容器会承担这一职责;否则应在镜像中加入 tini。如果 ps 中不断出现 Z 状态,就是这个问题。
设计优雅终止
1. SIGTERM 수신
2. 새 요청 받기를 멈춘다 (헬스 체크를 실패로 바꾼다)
3. 진행 중 요청이 끝나기를 기다린다
4. 연결·파일을 닫고 종료
第二步尤其重要。Kubernetes 删除 Pod 时,会同时移除端点并发送 SIGTERM,但端点变更传播需要数秒。在这段时间内到达的请求仍可能被发送到正在终止的 Pod,因而失败。在 preStop 钩子中加入短暂的 sleep,可以关闭这个时间窗口。
lifecycle:
preStop:
exec: {command: ["sh", "-c", "sleep 5"]}
terminationGracePeriodSeconds: 30
下个练习将做什么
分别启动带有处理器和不带处理器的容器,亲自制造退出码 143 与 137,并检查应用退出码和重启策略。