Docker 已启用 init,仍有僵尸进程:先检查 PPID
init存在但Z状态子进程仍属于活着的应用时,应修父进程的回收逻辑。核对PPID和命名空间,验收多轮任务退出而非只看重启。
容器已经配置 init,进程列表却仍有 <defunct> 或状态为 Z 的子进程。如果这些子进程的父进程还活着,继续增加一个 init 通常解决不了问题:退出状态仍应由它们自己的父进程收取,init 不能代替任意在运行的父进程执行 wait。
下面针对使用独立 PID 命名空间的 Linux 容器。Docker 自 1.13 起支持 --init,默认内置 init 基于 Tini;自行使用其他进程管理器时,也需核对其实际回收与信号行为,而不能只凭名字判断。
僵尸进程的父进程是谁
子进程退出后,父进程通常需要通过 wait、waitpid 或语言提供的相应接口读取退出状态。仍未收取的退出状态会保留成僵尸记录。它已经不是一个继续执行代码的工作进程,向这个僵尸 PID 再发 SIGKILL 不能代替父进程的回收。
可以用镜像已有的进程工具检查。以下要求容器内的 ps 支持所示选项,task-app 为占位名称:
docker exec task-app ps -eo pid,ppid,stat,comm
docker inspect task-app --format '{{json .HostConfig.Init}} {{json .Config.Entrypoint}} {{json .Config.Cmd}}'
重点核对 Z 状态对应的 PPID,再找到该父进程及实际启动关系。例如 init 是 PID 1、应用是 PID 7,而某个 Z 状态子进程的 PPID 仍是 7,就应检查应用的子进程管理;不能因为 init 在树的最上层,就认定这个子进程已经交给它回收。
这些数字仅为示意。要在同一 PID 命名空间中比较,不能把 docker top 的宿主编号混入容器内 ps。精简镜像没有 ps 时,使用现有监控或授权的宿主机工具,不必为查看把应用改成特权容器。
init 回收的是它自己的子进程和接管的孤儿
父进程退出后,遗留子进程会重新归属相应的 init 或子进程接管者。Tini 作为 PID 1,或按要求注册为 subreaper 时,可以回收交给它的退出子进程。Tini 的 waitpid 调用仍遵循父子关系,不会越过活着的应用去收取该应用的子进程。
因此,父进程结束后 Z 状态消失,不证明原应用的回收逻辑已经正常;它可能只是把这项责任留给了接管者。容器关闭后记录消失,也不能代替对运行期间资源积累的检查。
若 Z 状态进程已经直接归属 init,却长期不被回收,再检查这个 init 是否真正运行在合适的位置、是否具备接管条件及是否仍正常执行。创建配置中的 Init 值只是声明,还需对照进程树;已有包装入口和多层进程管理器可能改变关系。
修正产生子进程的应用
已确认父进程还在运行时,应沿代码找到启动子任务的位置,检查正常退出、失败、超时和取消分支是否都收取退出状态。多数语言有自己的子进程对象、wait 或 communicate 接口,按所用库的契约处理,不能只在成功分支调用一次。
异步程序还要检查负责回收的任务是否被取消、异常是否让后续处理提前退出。只启动后台子进程而不保存管理对象,或者超时后发送信号却不等待其结束,都值得结合实际代码核对。不能根据进程树直接断定是哪一行代码出错。
保留原启动配置,在隔离环境让相同任务经历正常结束、异常和超时,再比较子进程退出后的状态。不要为证明 init 能接管而在生产强杀父进程,也不要把故意留下僵尸的演示程序放进长期服务。
init 仍可以承担信号转发与孤儿回收,应用修正后无需机械删除它。若实际问题是服务直接作为 PID 1、收到 Ctrl-C 却不退出,可另看PID 1 的信号处理,那与活着的父进程未 wait 是不同的处理路径。
在父进程继续服务时检查回收
验收应观察原应用保持运行、多轮子任务退出后 Z 状态没有持续累积,并确认每项任务的退出结果按应用规则被处理。仅重启一次容器,看到进程列表暂时干净,不足以证明问题消失。
正式发布保留原镜像和部署配置,按既有流程替换受影响服务,并处理在途任务。需要回退时使用原版本与对应配置;已经中断或重复执行的业务任务需单独恢复,进程列表恢复不会撤销它们的业务影响。


