问题排查 - Java Process 调用的僵尸进程问题

481

问题

用 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 的 killkill -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 进程