摘要:容器的生老病死在进程层面都有确切对应:run 是创建加启动,stop 是发信号加等待,pause 是冻结 cgroup。本节拆解每条生命周期命令的真实动作,讲透 PID 1 的信号困境与僵尸进程问题,并处理"一启动就退出""杀不掉"两类高频故障。
把生命周期命令翻译成进程语言:
-s 指定信号)-f所以"容器活着"的确切含义是:容器内的 1 号进程活着。1 号退出,容器即退出。这条规则解释了无数新手困惑。
docker run ubuntu echo hello
输出 hello 后容器退出。这不是故障:echo 是前台进程,执行完自然退出,容器随之结束。反过来,如果 Dockerfile 里写了 CMD service nginx start,service 把 nginx 派生到后台就返回,1 号进程退出,容器立刻死亡——这就是"为什么容器里起服务不能用后台方式"的根源。容器的正确姿势是前台常驻:CMD ["nginx", "-g", "daemon off;"],1 号就是 nginx 本尊。
排查"一启动就退出"的标准流程:
docker logs <容器> docker inspect <容器> --format '{{.State.ExitCode}} {{.State.Error}}'
日志与退出码几乎总能给出答案:配置文件路径不对(退出码 1)、依赖的数据库连不上(应用自毁)、或者如上所述 1 号进程本身就不是常驻进程。
docker stop 发 SIGTERM 给 1 号进程,期待它处理信号、清理资源、退出。前提是信号真的能到达你的应用。两种典型断路:
断路一:shell 形式的 CMD。 CMD python server.py 实际 1 号是 /bin/sh,sh 不转发信号给子进程,你的应用永远收不到 SIGTERM,只能等 10 秒后被 SIGKILL 强杀,来不及落盘与断连。解法是 exec 数组形式,2.2 节已经立过规矩。
断路二:应用没装信号处理器。 收到 SIGTERM 按默认行为立即退出,连接中的请求被硬切。成熟框架都提供优雅退出钩子,把处理器接上。
PID 1 的另一职责:收割僵尸。 容器内 1 号进程是所有孤儿进程的养父,内核把孤儿托付给它,期待它 wait 回收。如果你的应用不调用 wait(多数业务程序不做这件事),它派生的子进程退出后就变成僵尸,积累到 pids 上限,容器就再也无法创建新进程。官方基础镜像里那些 --init 参数、tini 小工具干的就是这个:当专业 1 号,替你转发信号加收割僵尸:
docker run -d --init myapp
一行参数换来正确的信号语义与干净的进程表,生产环境值得默认加上。
偶尔遇到容器卡在 Removing 状态杀不掉,通常与宿主机存储或内核状态有关。处置顺序:
# 先看真实进程还在不在 ps -ef | grep <容器id> # 1 号还在:直接对真实 PID 动手 kill -9 <真实PID> # 进程已死但容器状态卡住:等几秒再试 rm -f docker rm -f <容器>
批量清理现场用组合拳(注意先确认再执行):
# 清理全部停止的容器 docker container prune -f # 清理悬挂镜像 docker image prune -f
pause 的一个实用场景:宿主机要做内存压力测试或快照,docker pause 冻结容器、不杀进程,测完 docker unpause 原样恢复。与 stop 的区别在于恢复成本——pause 是暂停音乐,stop 是关机重启。
docker run -d --restart=on-failure:5 myapp
三种策略:no(默认,死了不管)、always(守护进程启动也拉起)、on-failure:N(仅非零退出码重启,最多 N 次)。注意 restart 治标不治本:应用有 bug 反复崩溃,重启策略只会制造"看起来活着"的假象,根因还得靠日志与退出码排查。真正的自愈要在编排层(第 6 章)做健康检查驱动的重启。
运行时层拆完。接下来向上走到容器与外界连接的那一层——网络。