You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

主进程未终止子进程:Docker容器中大量defunct ssh进程的解决方法

问题描述

我有一个每隔约5分钟执行一次ssh的进程,代码如下:

private String runProcess() throws IOException {
   ProcessBuilder pb = new ProcessBuilder("sh","-c","ssh hostname echo hello");
   Process process = pb.start();
   if(!process.waitFor(10, TimeUnit.SECONDS)) {
            LOGGER.info("Taking too long");
            process.destroy();
   }
    StringBuilder output = new StringBuilder();
    BufferedReader reader = new BufferedReader(
                new InputStreamReader(process.getInputStream()));
    String line;
    while ((line = reader.readLine()) != null) {
            output.append(line).append('\n');
    }
    int exitVal = process.waitFor();

    if (exitVal != 0) {
            throw new RuntimeException(String.valueOf(exitVal));
    }
    process.destroy();
    return output.toString();

}

/// more code to do something with output of the ssh.

目前ssh进程运行正常,能获取预期输出且退出值为0。但在代码运行的Docker容器中执行ps aux后,发现大量ssh命令进程处于'defunct'状态,推测这些是上述方法中启动进程的子进程。请问如何在该方法中同时终止这些子进程?

解决方案

1. 直接启动SSH进程,避免sh -c中转

问题根源在于你通过sh -c启动ssh,此时Java进程的子进程是sh,ssh是sh的子进程。当sh退出后,若Java未正确回收ssh的退出状态,就会产生僵尸进程。直接启动ssh可让Java成为ssh的父进程,waitFor()会自动处理其退出状态:

ProcessBuilder pb = new ProcessBuilder("ssh", "hostname", "echo", "hello");

2. 若必须用sh -c,销毁整个进程组

如果因命令逻辑复杂必须使用sh -c,可将sh及其子进程(ssh)归入同一进程组,销毁时直接杀掉整个进程组,同时确保回收所有进程的退出状态:

private String runProcess() throws IOException, InterruptedException {
    ProcessBuilder pb = new ProcessBuilder("sh", "-c", "ssh hostname echo hello");
    // 合并错误流到输入流,避免进程因输出缓冲区满阻塞
    pb.redirectErrorStream(true);
    Process process = pb.start();

    // 读取输出(必须读取,否则进程可能挂起)
    StringBuilder output = new StringBuilder();
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
        String line;
        while ((line = reader.readLine()) != null) {
            output.append(line).append('\n');
        }
    }

    // 等待进程结束,超时则销毁整个进程组
    if (!process.waitFor(10, TimeUnit.SECONDS)) {
        LOGGER.info("Taking too long");
        // 获取进程PID,发送信号到整个进程组
        long pid = ((ProcessHandle) process.toHandle()).pid();
        // 先发送SIGTERM优雅终止,无效则换SIGKILL(信号9)
        Runtime.getRuntime().exec("kill -TERM -" + pid);
        // 等待进程组完全退出
        process.waitFor();
    }

    int exitVal = process.exitValue();
    if (exitVal != 0) {
        throw new RuntimeException(String.valueOf(exitVal));
    }

    return output.toString();
}

说明:kill -TERM -<pid>中的负号表示操作整个进程组,所有属于该组的子进程都会收到终止信号,后续waitFor()会回收它们的退出状态,不会产生僵尸进程。

3. 确保完整读取所有输出流

如果进程的错误流未被读取,可能导致进程阻塞无法正常退出,进而变成僵尸。务必通过redirectErrorStream(true)将错误流合并到输入流,或单独开启线程读取错误流,避免进程挂起。

内容的提问来源于stack exchange,提问作者MKRabbit

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 20:24:31