问题排查 - Java Process 调用的僵尸进程问题
问题
用 java 做了一个 服务,在其中用如下方式 Process 启动一个 python 进程,在业务发送「停止任务」请求时,服务会调用 Process#destroy() 关闭进程,出现了许多僵尸进程
我们可以 bash exec 到 pod 里面,使用 top 命令得到片段:
top - 10:02:40 up 18 days, 13:59, 0 users, load average: 1.59, 1.10, 0.71
Tasks: 54 total, 13 running, 15 sleeping, 0 stopped, 26 zombie
%Cpu(s): 16.9 us, 22.2 sy, 0.0 ni, 60.8 id, 0.2 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 65706812 total, 59366940 free, 3490760 used, 2849112 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 61558252 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
8442 root 20 0 60.0m 2.2m 1.6m R 0.7 0.0 0:00.36 top
7672 root 20 0 59.9m 2.2m 1.6m S 0.3 0.0 1:07.75 top
1 root 20 0 14.3g 359.2m 23.0m S 0.0 0.6 1:39.86 java -Dfile.encoding=UTF-8 -jar /etc/xxx-1.0.0.jar
78 root 20 0 16.6m 1.9m 1.5m S 0.0 0.0 0:00.02 /bin/bash
92 root 20 0 59.9m 2.2m 1.6m S 0.0 0.0 1:22.25 top
170 root 20 0 16.6m 2.0m 1.5m S 0.0 0.0 0:00.02 /bin/bash
249 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:01.64 [python3] <defunct>
250 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.09 [python3] <defunct>
6846 root 20 0 16.7m 2.1m 1.6m S 0.0 0.0 0:00.07 /bin/bash
7658 root 20 0 16.6m 2.0m 1.5m S 0.0 0.0 0:00.01 /bin/bash
7856 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:01.46 [python3] <defunct>
7857 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.08 [python3] <defunct>
8428 root 20 0 16.6m 2.0m 1.5m S 0.0 0.0 0:00.02 /bin/bash
8624 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:01.51 [python3] <defunct>
8625 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.06 [python3] <defunct>
8626 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:02.62 [python3] <defunct>
8627 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:02.65 [python3] <defunct>
8628 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.36 [python3] <defunct>
8629 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.33 [python3] <defunct>
8630 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.37 [python3] <defunct>
8631 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.36 [python3] <defunct>
8632 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.37 [python3] <defunct>
8633 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.37 [python3] <defunct>
8634 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.37 [python3] <defunct>
8635 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.32 [python3] <defunct>
8636 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.36 [python3] <defunct>
8637 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.35 [python3] <defunct>
8638 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.36 [python3] <defunct>
8662 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.34 [python3] <defunct>
8663 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.37 [python3] <defunct>
8664 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:00.34 [python3] <defunct>
9062 root 20 0 0.0m 0.0m 0.0m Z 0.0 0.0 0:01.76 [python3] <defunct>
如何复现
首先,我们 bash exec 到 pod 里面,使用 top 命令监听进程启动。接着,去业务层启动多个任务,每个 java 的任务会实例化一个 Process 对象,CMD 调用 Python 命令,这个 Python 进程会启动几个子进程,如果 top 中看到许多 Python 子进程启动,此时在业务层点击停止所有任务,那么 Python 子进程就会变成 [python3] <defunct>
,成为一个僵尸进程
过程分析
java kill 的代码:
public void destroy() {
if (!Objects.isNull(process)) {
process.destroy();
try {
int timeout = 30;
log.warn("停止进程【{}】开始...", process.pid());
boolean isSuccessExisted = process.waitFor(timeout, TimeUnit.SECONDS);
log.warn("停止进程【{}】完成,是否【{}】S 内成功退出:【{}】...", process.pid(), timeout, isSuccessExisted);
if (!isSuccessExisted) {
log.warn("停止进程【{}】未在指定时间内完成", process.pid());
process.destroyForcibly();
}
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
Thread.currentThread().interrupt();
}
// 获取所有子进程,发出退出信号量
process.descendants().forEach(ProcessHandle::destroy);
}
}
我们这里每次从日志其实可以看到每次,拿到的结果都是 true,也即是没有做强关,但 Python 子进程就是变成僵尸进程了
反复启动任务,top 查看,等子进程创建出来后,再停止,多次后,得到以下现象 & 分析:
- 1、java 服务 pid 为 1, Process 调用 CMD,会启动一个 Python 进程调用
- 2、这个 Python 进程会利用到 locky 进程池技术,启动一堆进程,这些进程的 PPID 归属就是第一步启动的那个 Python 进程
- 3、业务调用停止任务时,java 服务会通过发退出信号给第一步启动的 Python 进程,等待 Python 响应 true,就成功返回
- 4、然后 Python 进程退出了,但其启动的一堆子进程没有正确退出,变成了僵尸进程,且 PPID 归属变了,最后它们的 PPID 变成了 1 ,即 java 服务 变成了其父进程
原因推测
Java 发出的进程关闭信号量只有自己实例化的进程收到了,这个进程创建的子进程没有收到信号量,或者说没有去处理它,于是子进程跑着跑着发现父进程挂了(因为 java 服务通过 Proces#destry()
调用了),那么子进程会被 init 进程收养,在 Linux 中,init 进程是 PID 为 1 的进程,刚好就是 java 服务,但是 java 服务没有去管理这些子进程,没有去处理这些子进程的退出信息,那么子进程变成了僵尸进程
解决方案
让子进程被一个能管理它们退出信息的进程收养,那么要求 init 进程必须得具备这个能力,tini 这个程序就是专门干进程管理的这些事情的
1、下载:wget https://github.com/krallin/tini/releases/download/v0.19.0/tini-amd64,将程序放入工程
2、docker copy tini 程序:dockerfile:COPY dockerfile/tini-amd64 /bin/tini-amd64
3、启动服务用 tini 启动,-g 管理进程组:dockerfile:ENTRYPOINT ["/bin/tini-amd64", "-g", "--", "/java-service-entrypoint.sh"]
如此,tini
会成为 init 进程
,并收养 Python locky
产生的孤儿进程,将其释放,防止其出现 Z 进程
验证
经过实测,可以解决僵尸进程问题
后记 & 优化
实测来看 Process#destroy()
和 Process#destroyForcibly()
和 Linux 的 kill
和 kill -9
似乎没有什么关系,原本推测是一一对应,但实测来看, java 对于进程的停止和 Linux 的是不一样的
我们碰到另一一个问题是,子进程可能会大量处于 S 状态,休眠等待。虽然这些 S 进程醒了后,也不会变成 Z 进程,可以正常退出,但会大量占用物理资源。即使 java 服务使用 Process#destroyForcibly()
或者 对其后代强制 destroy process.descendants().forEach(ProcessHandle::destroyForcibly);
,也依然大量存在 S 子进程。针对这个问题,我的解决方案是拿到进程的所有子进程 PID,process.descendants().map(ProcessHandle::pid).toList();
,加上进程自己的 PID,按照空格拼接 PID 为一个字符串,再启动一个 Proces 对象去跑 kill -9
这些 PID ,这样子进程是会被直接 kill 且收养的,并且没有出现僵尸进程,不过如果把 tini 去掉,那么这里效果是,子进程会直接变成 Z 进程