PID 1 的两个特殊行为
容器内的 PID 1 与普通进程不同:内核不会对它应用默认的信号处理,未显式注册处理函数的信号会被忽略;同时它承担着回收孤儿进程的责任,不回收就会留下僵尸。应用如果没有处理 SIGTERM,docker stop 的优雅停止就无从触发,只能等超时后被 SIGKILL 强杀,表现为停机变慢、连接被硬断、数据来不及刷盘。
基本信息
| 问题 | PID 1 忽略未显式处理的信号;僵尸进程无人回收 |
|---|---|
| 典型表现 | docker stop 后进程不退,默认等待约 10 秒后收到 SIGKILL,退出码 137 |
| 方案 A | 应用直接作为 PID 1(exec 形式启动),并自行处理 SIGTERM |
| 方案 B | docker run --init 注入 tini:转发信号并回收僵尸进程 |
| 超时调优 | docker stop -t 30 延长优雅停止窗口,给足收尾时间 |
| Kubernetes | terminationGracePeriodSeconds 对应同一窗口,preStop 钩子可先摘流量再停进程 |
典型使用场景
- 需要优雅下线的 Web 服务:等在途请求处理完再退出,避免用户看到 502
- 数据库与消息消费者:退出前刷盘、提交位点,防止重复消费或数据不一致
- Kubernetes 滚动更新:Pod 终止时先把流量端点摘除再停进程
- 批处理任务:收到中断信号后落盘进度,下次可从断点继续
- 以 shell 脚本作为入口的镜像:最常见的问题来源,脚本不退位应用就永远收不到信号
使用与验证
使用与验证
docker run -d --init --name app myapp:1.0 # 注入 init 转发信号、回收僵尸
docker stop -t 30 app # 给 30 秒优雅停止窗口
docker kill -s TERM app # 直接发 SIGTERM,验证应用是否响应
docker inspect -f '{{.State.ExitCode}}' app # 看退出码判断是否被强杀
# 入口脚本必须以 exec 结尾,让应用接管 PID 1
#!/bin/sh
echo starting
exec "$@" # 少了 exec,信号会停在脚本层退出码与现象读法
| 现象 | 含义 | 处理方式 |
|---|---|---|
| 退出码 137 | 128+9,被 SIGKILL 强杀 | 查优雅超时是否过短,或内存上限触发 OOM |
| 退出码 143 | 128+15,收到 SIGTERM 后退出 | 属正常优雅停止 |
| 退出码 0 | 进程正常结束 | 结合重启策略判断是否符合预期 |
| 停容器耗时约 10 秒后强杀 | 默认超时内未退出 | 优先修信号处理,而不是只调大超时 |
| defunct 僵尸进程 | PID 1 未回收子进程 | 加 --init,或让入口脚本 exec 应用 |
shell 入口必须以 exec "$@" 结尾让应用接管 PID 1,否则信号永远停在脚本层。这是「明明在应用里写了 trap 却不生效」的最常见原因:脚本没转发,应用根本没收到信号。
用 SIGKILL 直接关闭容器内应用会跳过刷盘与连接收尾,数据库、消息队列、文件上传这类场景可能出现数据不一致。应急之外不要养成 kill -9 关闭容器的习惯,优先 SIGTERM 并留足超时。