容器里的服务收不到 SIGTERM?先查 PID 1 与 shell 包装
现象
容器执行 docker stop,或在 Kubernetes 中删除 Pod 时,应用没有打印退出日志,进程总是撑到宽限时间后收到强制信号。此时 docker inspect 的 ExitCode 可能显示 137,很容易被误判为 OOM。137 只代表进程被信号终止,不等于一定是内存问题。
最小复现
下面用一个 shell 包装业务进程,并在启动前打印环境信息:
#!/bin/sh
echo start
python app.py
如果镜像写成 CMD ./start.sh,容器的 PID 1 往往就是 shell 或入口脚本,而不是 python。停止信号被发给 PID 1 后,shell 不再把信号转给子进程,应用便没有机会执行连接关闭、任务收尾等清理动作。
先在容器里核实进程关系:
cat /proc/1/cmdline
ps -eo pid,ppid,comm,args
docker inspect -f '{{.Config.Cmd}}' <容器名>
如果 PID 1 是 sh、bash 或某个 entrypoint 脚本,而业务进程是它下面的子进程,就应该先怀疑包装层。再观察日志中是否同时出现“优雅退出”和“强杀”痕迹:应用自己打印了关闭日志,说明信号已收到,问题转向清理超时;完全没有关闭日志,则重点查 PID 1 和进程树。
修复方式
启动脚本的最后一行用 exec 启动业务,让业务进程替换 shell 并成为 PID 1:
exec python app.py
Dockerfile 中的命令优先使用 exec 形式,避免多套一层 shell:
CMD ["python", "app.py"]
exec 形式并不是万能药。入口脚本前面如果有初始化动作,最后仍要写成 exec "$@";业务本身会大量 fork 子进程时,可以用 tini 或容器运行时的 init 选项充当 PID 1,负责回收孤儿进程。
验证与预防
修复后用 docker stop --time 5 测退出耗时,并让应用显式处理 SIGTERM 和 SIGINT:停止接收新请求、等待在途请求、释放连接,超过内部截止时间再退出。Kubernetes 中还要检查 terminationGracePeriodSeconds 和 preStop 是否设置了不合理的长等待。
排查顺序可以固定为:先区分 OOM 与普通强杀,再确认 PID 1 是谁,接着检查 shell 和脚本是否转发信号,最后看应用 handler 与清理超时。直接加大宽限期会掩盖问题,短期看似稳定,长期仍会制造发布抖动。
把这个检查加入镜像模板和启动脚本评审,通常比事故后再追 ExitCode 更有效。
- 点赞
- 收藏
- 关注作者
评论(0)