根本解法是让容器pid 1具备init职责:主动回收僵尸进程、正确转发信号;推荐用--init参数启用tini,或在dockerfile中安装tini并设为entrypoint,配合exec启动方式确保信号与僵尸管理生效。

根本解法是让容器 PID 1 具备 init 职责:主动回收僵尸进程、正确转发信号。默认的 shell(如 /bin/sh)或普通应用不处理 SIGCHLD,子进程退出后便滞留为 Z 状态,长期积累会耗尽 PID 表项,甚至导致 fork: Cannot allocate memory 错误。
用 --init 启动容器(最简推荐)
Docker 1.13+ 原生支持 --init 参数,底层自动注入 tini——一个轻量、可靠、官方推荐的容器专用 init:
- 运行时启用:
docker run --init -d myapp - 无需修改镜像,适合测试、CI 或临时修复
- 自动作为 PID 1 运行,持续调用
waitpid(-1, ...)清理所有子进程退出状态 - 将
SIGTERM、SIGINT等信号完整转发给主应用进程
在 Dockerfile 中固化 tini
若需构建可复现、生产就绪的镜像,应在构建阶段集成 tini:
- Debian/Ubuntu 基础镜像:
RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/* - Alpine 镜像:
RUN apk add --no-cache tini - 设置入口点:
ENTRYPOINT ["/sbin/tini", "--"] - 后续 CMD 会作为 tini 的子进程启动,全程受其托管
避免启动方式引入隐性 PID 1 陷阱
即使用了 tini,错误的启动写法仍会让 shell 抢占 PID 1,绕过 init 管理:
- ❌ 错误(shell 模式):
CMD ./start.sh→ 实际执行/bin/sh -c "./start.sh",/bin/sh成为 PID 1 - ✅ 正确(exec 模式):
CMD ["./start.sh"]或脚本末尾用exec "$@"替换当前 shell - Java/Node.js 等应用中,避免用
Runtime.exec()或child_process.spawn()启动长期子进程却不监听exit事件
验证与排查方法
进入运行中的容器,快速确认是否真正生效:
- 查 PID 1 是什么:
ps -p 1 -o comm=—— 应显示tini或dumb-init,而非sh、bash - 查僵尸数量:
ps axo stat | grep -c '^Z'—— 稳定为0才算有效 - 压力测试:循环启动退出子进程
for i in $(seq 100); do sh -c 'sleep 0.01' & done,再检查 Z 进程数











