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;
}
}
}
}