Linux 僵尸进程排查通用笔记

Linux 僵尸进程排查通用笔记

1. 基本认识

僵尸进程是已经退出、但退出状态尚未被父进程通过 wait() / waitpid() 等接口回收的进程。ps 的 STAT 以 Z 开头,例如 Z、Zs;s 表示会话首进程,不改变僵尸状态。

僵尸不再执行代码,通常不消耗用户态 CPU 和原来的应用内存,但仍保留 PID 和少量内核记录。大量积累可能影响进程/线程创建。不要用“占用 CPU 高”解释僵尸,也不要把 D(不可中断睡眠)当作僵尸。

排查顺序:确认数量 → 按父进程聚合 → 确认父进程及容器/服务 → 检查增长与限制 → 评估处理范围 → 验证回收和业务。

以下排查命令以 Linux、procps 和 Bash 为例,默认只读;权限不足时仅对必要命令使用 sudo。命令参数、日志和容器配置可能含密钥,分享前脱敏。

2. 快速确认与统计

date -Is
hostname

# 列出僵尸;不读取完整命令参数,减少敏感信息暴露
ps -eo pid,ppid,stat,lstart,etime,user,comm |
  awk 'NR==1 || $3 ~ /^Z/'

# 统计总数,无僵尸时输出 0
ps -eo stat= | awk '$1 ~ /^Z/ {n++} END {print n+0}'

# 按当前父 PID 和进程名称统计
ps -eo ppid=,stat=,comm= |
  awk '$2 ~ /^Z/ {a[$1 " " $3]++} END {for (k in a) print a[k], k}' |
  sort -nr

PPID 是当前父进程,可能是原始启动者,也可能是孤儿进程被收养后的回收者,不能仅凭 PPID 断定谁最初创建了子进程。lstart / etime 是进程启动时间和从启动至今的时长,并不是变成僵尸的时间。

3. 追踪父进程

将下面 PID 替换为统计得到的父 PID,每次操作前重新核对身份,避免 PID 复用造成误判。

parent_pid=12345
ps -p "$parent_pid" -o pid,ppid,stat,lstart,etime,pcpu,pmem,nlwp,comm
ps --ppid "$parent_pid" -o pid,ppid,stat,etime,comm
ps --ppid "$parent_pid" -o stat=,comm= | sort | uniq -c

readlink "/proc/$parent_pid/exe"
readlink "/proc/$parent_pid/cwd"
awk '/^(Name|State|PPid|NSpid|Threads|SigIgn|SigCgt):/' "/proc/$parent_pid/status"
cat "/proc/$parent_pid/cgroup"

# 安装了 pstree 时查看祖先链,避免一次展开所有子进程
pstree -sp "$parent_pid"

NSpid 在宿主机视角可显示各层 PID 命名空间中的 PID;末尾为 1 表示该进程在最内层命名空间是 PID 1。容器 PID 1 需要正确处理子进程回收,普通 sleep 不具备完整的 init 回收能力。信号位图仅是辅助证据,不能据此证明程序是否正确调用了 wait。

必要时在受控终端查看 ps -p "$parent_pid" -o args=,不要直接把结果贴到公共记录。无需读取 /proc/PID/environ 或完整容器配置来完成基本归属确认。

4. 确认容器或服务归属

# Docker 或兼容客户端:只列出 ID 和名称
docker ps --no-trunc --format '{{.ID}} {{.Names}}'

# CRI 环境
crictl ps -a

# containerd:先核实实际 namespace,k8s.io 只是常见值
ctr namespaces list
ctr -n k8s.io containers list

# systemd 服务:可能展示日志/命令参数,输出需脱敏
systemctl status "$parent_pid" --no-pager --lines=0

用 /proc/PID/cgroup 中的容器 ID 与运行时列表交叉匹配。路径中出现 k8s.io 不足以证明由某个 Kubernetes Pod 管理;应进一步检查选定容器的 Pod 名称、namespace 和控制器信息。不要把整个 inspect JSON 导出,因为其中可能包含环境变量或凭据。

5. 判断是否增长及资源影响

# 采样 6 次,每次间隔 10 秒;按 Ctrl+C 可停止
for sample in {1..6}; do
  date -Is
  ps -eo stat= | awk '$1 ~ /^Z/ {n++} END {print "zombies=" n+0}'
  sleep 10
done

# 系统进程数量及全局上限
ps -e --no-headers | wc -l
cat /proc/sys/kernel/pid_max
cat /proc/sys/kernel/threads-max

# 父进程继承的资源限制
cat "/proc/$parent_pid/limits"

系统进程总数与 pid_max 的比值只能粗略参考;线程数、用户限制和 cgroup PID 限制也会影响创建能力。ulimit -u 查询的是当前 shell 的限制,不能直接代表目标服务。

对于 cgroup v2(先用 stat -fc %T /sys/fs/cgroup 确认是 cgroup2fs):

cg_path=$(awk -F: '$1=="0" {print $3}' "/proc/$parent_pid/cgroup")
if [[ -n "$cg_path" ]]; then
  for item in pids.current pids.max pids.events; do
    file="/sys/fs/cgroup${cg_path%/}/$item"
    if [[ -r "$file" ]]; then
      printf '%s: ' "$item"
      cat "$file"
    fi
  done
fi

需要在宿主机对应的 cgroup 挂载视角读取。逐级检查父 cgroup,直到挂载根;叶子 pids.max=max 不代表祖先无限制。pids.current 统计任务,包含线程;pids.events 的 max 反映该范围的限额触发事件,应结合内核版本、层级和采样变化解释。cgroup v1 的挂载位置和路径不同,不能照搬上述路径。

同时检查业务是否出现 Resource temporarily unavailable、fork / clone 失败、任务无法启动。日志应限定服务和时间范围,不要无差别扫描或输出所有日志。

6. 原因分析与处理原则

现象 优先核对
僵尸集中在一个业务父进程下 子进程启动、异常分支、超时退出后是否调用 wait;后台任务是否遗漏回收
容器 PID 1 是 sleep 或未处理回收的脚本 孤儿子进程是否被 PID 1 收养;入口是否配置合适的 init/reaper
ssh 等短命进程周期性积累 定时任务、并发执行框架和取消/超时路径的回收逻辑
重启后数量归零但再次增长 临时清理成功,程序问题仍未解决

kill -9 对僵尸本身不能完成回收,因为它已经退出。向父进程发送 SIGCHLD 也不保证有效:程序必须具有正确的处理与 wait 逻辑。不要默认批量发信号或杀父进程。

处理通常有两个层次:

  1. 程序修复:对自己创建的子进程在正常、异常、超时和取消路径调用 wait/communicate 等适用接口;正确管理后台任务。init/reaper 可以回收被收养的孤儿,但无法替仍存活的业务父进程回收其直接子进程。
  2. 运行恢复:确认业务任务、持久化、依赖、维护窗口和服务管理方式后,通过实际管理者有序重启目标服务或容器。容器 PID 1 退出可能终止容器内其他进程,不能当作无影响清理。Kubernetes 环境先检查控制器、可用副本及中断约束。

处理后重新统计总数和父 PID 分布,检查业务及任务状态,并跨越原定时任务周期观察是否再次积累。记录执行时间、对象、恢复结果和剩余问题。

7. 本次只读现场记录(2026-10-26)

访问路径:本机 ssh qiaoning 后,在跳板机执行 ssh aistack;目标主机名为 http-node。本机 ssh -J qiaoning aistack 未成功,跳板端解析目标名称失败;嵌套 SSH 成功,说明两种连接方式不能直接等同。

14:52 左右(+08:00)两次采样均为 1,872 个僵尸进程:

当前父 PID 父进程 容器名称 僵尸子进程
2891049 python,容器内 PID 1,76 线程 inspection 1,848 个 ssh
2287464 ops / python3.11,容器内 PID 1 aistack-ops-mcp 18 个 ssh
3306832 sleep,容器内 PID 1 dongchang-20261013 2 个 dongchang、2 个 crane
2696644 sleep,容器内 PID 1 dongchang-20260717 1 个 dongchang
1827059 sleep dongchang-20260831 1 个 dongchang

容器名通过 cgroup ID 与 docker ps 列表匹配,containerd 列表也能找到对应 ID。crictl 查询未返回匹配容器,未据此推断 Pod 归属。

可见部分 ssh 的启动日期跨越多天,且最新日期为当天;这支持存在历史积累,但启动时间不能证明成为僵尸的时间,也不能由两次短时采样推断每天增长量。

采样时宿主机进程数约 2,825,pid_max=4194304,threads-max=4125181。inspection 对应 cgroup 的 pids.current=1924、pids.max=max、pids.events 为 max 0;其余四个容器对应值分别为 21、5、2、3,均为 pids.max=max、max 0。直接上级 /k8s.io 为 pids.current=2065、pids.max=max、max 0。这些采样未显示已检查层级发生 PID 配额触发,但未完成所有用户/服务限制和业务日志的全面检查,不能宣称完全没有业务影响。

判断:当前父进程未回收这些已退出子进程;主要集中在 inspection,应优先检查其 ssh 执行和回收逻辑。sleep 作为容器 PID 1 的场景需要核对入口与孤儿回收机制。尚未检查业务源码,不能确定具体缺陷位置或认定某个启动参数就是根因。

本次未执行:kill、发送信号、容器/服务重启、配置修改或部署。后续应先核实 inspection 的当前任务和管理方式,再安排程序修复及必要的有序重启。

最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容