先记住这个答案
当容器启动时,Docker为容器创建新的PID命名空间,入口进程成为该命名空间内第一个进程,PID即为1。在Linux中,PID 1是内核直接处理信号的特例,并负责收养孤儿进程以回收资源。但容器默认没有init系统,若入口进程未处理这些职责,就会出现docker stop的SIGTERM未被有效处理,容器被强杀;或因不调用wait导致子进程变僵尸。解决方法是使用--init或让应用自己捕获信号并管理子进程。
- PID命名空间隔离使入口进程成为PID1
- PID1需处理信号和回收僵尸进程
- 缺少init时会出现强杀或僵尸问题
PID命名空间如何产生1号进程
当执行docker run时,Docker通过Linux的clone系统调用创建新的PID命名空间。该命名空间内的第一个进程被分配PID1,这个进程通常是镜像中ENTRYPOINT或CMD指定的命令。新命名空间屏蔽了宿主机其它PID,容器内进程只能看到自身命名空间内的进程。
Linux内核为PID1赋予独特的语义:当其它进程的父进程退出时,这些进程会被收养到PID1下;当子进程终止进入僵尸状态,PID1必须调用wait系统调用回收其进程描述符,否则僵尸进程无法释放。此外,内核在向整个命名空间发送信号时,PID1是唯一不能被SIGKILL或SIGSTOP直接杀死的进程,这需要特殊设计。
Java服务无法优雅停机
假设镜像中直接设置CMD ["java","-jar","app.jar"],运行后Java进程即为PID1。docker stop默认发送SIGTERM,而Java默认对SIGTERM的处理是立即退出,不等待线程池收尾,也不触发优雅关闭逻辑。这会导致未提交的事务丢失,日志中没有正常关闭记录,甚至产生不完整的输出。
正确做法是让Java注册Runtime.getRuntime().addShutdownHook,但工程上更可移植的是启动时加入--init参数,使tini作为PID1。tini捕获SIGTERM并转发给子进程(Java),同时不断wait回收其退出状态。此时容器内PID1是/tini,Java为PID2,信号和进程回收均被正确管理。
失效条件与应对方法
如果入口进程是shell形式(如CMD ['sh','-c','nginx']),shell作为PID1不会将SIGTERM转发给子进程,自身默认退出。shell退出后容器主进程结束,Docker会清理容器内所有进程,nginx可能来不及响应SIGTERM而被强制终止,导致非优雅关闭。类似情况还包括使用脚本启动多个守护进程而没有init机制统一管理,此时shell退出会导致所有守护进程被一并清理。
判断方法:容器内执行ps -ef,观察PID1是否仍是应用本身。若业务需要多进程或信号转发,必须显式提供init支持。常见选择是Docker的--init选项或改用带init的镜像基座。代价是额外约几MB内存和启动时间,但换来信号可靠性和僵尸回收。
容易答错的地方
- PID1在容器内与宿主机作用相同
- 事实是PID命名空间使容器内PID1只能见容器进程,但内核的逻辑(收养孤儿、信号特例)依然生效。容器入口进程不会自动获得init能力,它必须自己实现这些职责,否则出现同样问题。
- 僵尸进程无关紧要
- 僵尸进程本身不消耗CPU/内存,但会占用进程表项。容器内大量僵尸会导致进程号耗尽,且无法被普通进程回收,最终必须重启容器。只有PID1能通过wait回收,缺少处理机制就埋下资源耗尽风险。
面试官还会怎么问?
如何查看容器内PID1是哪个进程?
执行docker inspect <container>查看Pid,或进入容器运行ps -p 1 -o comm=。如果PID1不是init/tini,通常是业务直接作为入口进程。
docker stop具体发送什么信号?
docker stop先发送SIGTERM,默认等待10秒,超时后发送SIGKILL。可以通过--stop-signal和--stop-timeout自定义。
使用--init后,PID1如何工作?
tini作为PID1,负责捕获信号并转发给子进程,同时循环调用wait回收被收养的子进程,从而解决僵尸问题。这是它的核心功能。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。