Java创建进程读取Process.getInputStream阻塞问题

1. 问题现象

在某个 k8s pod 中,通过 Java 代码创建进程执行其他程序时,被阻塞了很久

2. 问题分析

2.1. 在 shell 中执行命令

将 Java 代码中执行的命令拷贝出来,在对应 k8s pod 的 Linux 操作系统 shell 中执行,在比较短的时间内能够执行完毕,不会阻塞,说明以上阻塞问题不是由于被执行的命令导致

2.2. Java 代码验证

将出现问题的代码拷贝到单独的 Java 类中,在对应服务器中运行,执行对应的命令时会被阻塞,可以重现问题,代友示例如下:

        try {
            String command = "...";
            System.out.println("### start");
            Process process = Runtime.getRuntime().exec(command);
            BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = in.readLine()) != null) {
                System.out.println(line);
            }
            in.close();
            System.out.println("### end");
        } catch (Exception e) {
            e.printStackTrace();
        }

2.3. stderr 重定向到 stdout

在 shell 中执行以上命令时,发现命令执行的输出无法重定向到文件中,说明对应的命令输出到了 stderr 而不是 stdout

因此将以上命令写入 shell 文件中,并在最后增加“ 2>&1”指定将 stderr 重定向到 stdout,在 Java 代码中执行以上脚本文件,能够正常结束,可以解决问题

2.4. 使用 java.lang.Process#getErrorStream 方法读取子进程输出数据

以上在创建进程后读取子进程输出数据时,使用了 java.lang.Process#getInputStream 方法获取输入流,对应的是子进程的 stdout

改为使用 java.lang.Process#getErrorStream 方法获取子进程的 stderr 对应的输入流,在 Java 代码中执行原有的命令,能够正常结束,可以解决问题

3. 问题对比

3.1. JDK 中关于子进程可能被阻塞的相关说明

在 java.lang.Process 类中有以下注释:

 * <p>By default, the created subprocess does not have its own terminal
 * or console.  All its standard I/O (i.e. stdin, stdout, stderr)
 * operations will be redirected to the parent process, where they can
 * be accessed via the streams obtained using the methods
 * {@link #getOutputStream()},
 * {@link #getInputStream()}, and
 * {@link #getErrorStream()}.
 * The parent process uses these streams to feed input to and get output
 * from the subprocess.  Because some native platforms only provide
 * limited buffer size for standard input and output streams, failure
 * to promptly write the input stream or read the output stream of
 * the subprocess may cause the subprocess to block, or even deadlock.

默认情况下,创建的子进程没有自己的终端或控制台。子进程所有的标准 I/O(即 stdin、stdout、stderr)操作将被重定向到父进程,在父进程中可以通过以下方法获得对应的流来访问:

java.lang.Process#getOutputStream()
java.lang.Process#getInputStream()
java.lang.Process#getErrorStream()

父进程通过这些流向子进程传输输入及获取输出。由于某些本地平台仅为标准输入输出流提供受限制的缓冲区大小,未能及时写入子进程的输入流或读取其输出流,可能会导致子进程阻塞,甚至死锁

以上说明应该是导致问题的原因

3.2. 其他服务器

在安装相同操作系统版本的其他 k8s pod、docker 容器、VM 中执行相同的代码,未重现以上问题

3.3. 重启 k8s pod

对于出现问题的 k8s pod,重启后问题仍然存在

4. 验证输出与读取相同及不同流的情况

使用以下代码,验证 Java 代码创建的子进程输出与 Java 代码读取相同及不同的流的情况

4.1. 代码

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.time.LocalDateTime;

public class TestProcess {

    public static void main(String[] args) {
        String command = args[0];
        run(command, "write_stdout".equals(args[1]), "read_stdout".equals(args[2]));
    }

    public static void run(String command, boolean writeStdout, boolean readStdout) {
        try {
            String used_command = command + (writeStdout ? " 2>&1" : " 1>&2");
            System.out.println("\r\n### start: [" + used_command + "]"
                    + (writeStdout ? " write_stdout " : " write_stderr ")
                    + (readStdout ? " read_stdout " : " read_stderr ")
                    + LocalDateTime.now());
            Process process = Runtime.getRuntime().exec(new String[]{"sh", "-c", used_command});
            BufferedReader in;
            if (readStdout) {
                in = new BufferedReader(new InputStreamReader(process.getInputStream()));
            } else {
                in = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            }

            String line;
            while ((line = in.readLine()) != null) {
                System.out.println(line);
            }
            in.close();
            System.out.println("### end: " + LocalDateTime.now());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.2. 验证内容说明

在执行的 Java 代码中会使用三个参数,分别如下

参数 1 用于指定需要执行的命令,以下通过 seq 命令输出足够长的内容

参数 2 用于指定将执行命令的 stderr 重定向到 stdout,还是将 stdout 重定向到 stderr,在执行命令时在最后拼接“ 2>&1”或“ 1>&2”实现

参数 3 用于指定 Java 代码从子进程的 stdout 还是 stderr 读取输出

4.3. 输出到 stdout,读取 stdout

java -cp . TestProcess ‘sh -c “seq -f “%04096g” 1 1”’ write_stdout read_stdout

4.4. 输出到 stderr,读取 stderr

java -cp . TestProcess ‘sh -c “seq -f “%04096g” 1 1”’ write_stderr read_stderr

4.5. 输出到 stdout,读取 stderr

java -cp . TestProcess ‘sh -c “seq -f “%04096g” 1 1”’ write_stdout read_stderr

4.6. 输出到 stderr,读取 stdout

java -cp . TestProcess ‘sh -c “seq -f “%04096g” 1 1”’ write_stderr read_stdout

4.7. 执行结果

在以上出现问题的 k8s pod 中,输出到 stdout,读取 stdout、输出到 stderr,读取 stderr,这两种情况下,在 Java 代码中执行命令时能够很快结束

验证情况验证结果
输出到 stdout,读取 stdout执行正常
输出到 stderr,读取 stderr执行正常
输出到 stdout,读取 stderr出现问题
输出到 stderr,读取 stdout出现问题

4.8. 输出内容大小

经过验证,在以上出现问题的 k8s pod 中,当子进程输出与 Java 代码读取的流不同时,Java 代码执行的命令输出内容大于等于 4096时,会一直阻塞;输出内容小于 4096时,能够很快结束

4.9. 子进程结束对 Java 代码的影响

当子进程因为被 kill 或其他原因结束时,Java 代码执行的从 java.lang.Process#getInputStream、java.lang.Process#getErrorStream 方法返回流读取数据的操作会结束阻塞

5. 问题解决

为了避免在某些服务器上 Java 代码创建进程执行其他程序(如 Python 等)时因为输出流缓冲区大小限制出现阻塞问题,需要对子进程的 stdout 与 stderr 都进行读取,可参考以下代码

以下会创建两个线程分别用于读取子进程的 stdout 与 stderr,等待线程执行完毕,并等待子进程执行完毕

在线程中进行操作前,可以获取父线程 MDC 中的信息并复制到当前线程的 MDC 中,便于在日志中打印 trace_id 等信息;执行完毕时对 MDC 中的信息进行清理

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.time.LocalDateTime;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

public class TestProcess2 {

    public static void main(String[] args) {
        String command = args[0];
        run(command, "write_stdout".equals(args[1]));
    }

    public static void run(String command, boolean writeStdout) {
        try {
            String used_command = command + (writeStdout ? " 2>&1" : "1>&2");
            System.out.println("\r\n### start: [" + used_command + "]"
                    + (writeStdout ? " write_stdout " : " write_stderr ")
                    + LocalDateTime.now());
            Process process = Runtime.getRuntime().exec(new String[]{"sh", "-c", used_command});
            int exitValue = ProcessRunWaiter.waitProcess(process);
            System.out.println("### end: exitValue " + exitValue + " " + LocalDateTime.now());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class ProcessRunWaiter {

        private static final ThreadFactory threadFactoryStdout = genThreadFactory("wait_stdout");
        private static final ThreadFactory threadFactoryStderr = genThreadFactory("wait_stderr");

        private static ThreadFactory genThreadFactory(String threadNamePrefix) {
            return new ThreadFactory() {
                private final AtomicInteger seq = new AtomicInteger(0);

                @Override
                public Thread newThread(Runnable r) {
                    Thread thread = new Thread(r);
                    thread.setDaemon(false);
                    thread.setName(threadNamePrefix + "-" + seq.addAndGet(1));
                    return thread;
                }
            };
        }

        public static int waitProcess(Process process) {
            Thread threadStdout = threadFactoryStdout.newThread(() -> {
                /* MDC 内容复制 */
                try (BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                    String line;
                    while ((line = in.readLine()) != null) {
                        System.out.println("stdout: " + line);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    /* MDC 内容清理 */
                }
            });

            Thread threadStderr = threadFactoryStderr.newThread(() -> {
                /* MDC 内容复制 */
                try (BufferedReader in = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
                    String line;
                    while ((line = in.readLine()) != null) {
                        System.out.println("stderr: " + line);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    /* MDC 内容清理 */
                }
            });

            threadStdout.start();
            threadStderr.start();

            try {
                threadStdout.join();
                threadStderr.join();
                return process.waitFor();
            } catch (InterruptedException e) {
                e.printStackTrace();
                Thread.currentThread().interrupt();
                return -1;
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值