这是一个 Java Web 程序,里面跑着一个定时任务,当任务触发时,程序需要调用一个外部的命令行工具,暂且叫它 external-tool
, 示例代码如下:
public static void main(String[] args) {
Process p = null;
try {
p = Runtime.getRuntime().exec("external-tool arg0 arg1 arg2");
int exitValue = p.waitFor();
if (exitValue != 0) {
System.out.println("Exec failed");
return;
}
System.out.println("Exec success");
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
if (p != null) {
p.destroy();
}
}
}
其中, arg*
在实际程序中是动态组装的,每次任务的参数列表不同。
测试过程中发现,某些特定的参数组合会造成程序 假死,表现为:Java web 程序不报错,不继续运行;通过查看系统进程,能够看到 external-tool
的进程,而且 CPU, 内存占用都很低。
初步调试发现:
- 由于
external-tool
工具自己会输出日志到本地文件以及标准输出,通过查看日志文件,发现每次程序 假死, 工具的日志都是卡在一个固定的位置,也没有报错。- 磁盘 IO 也正常,不存在日志文件无法写入的情况
- 单独把造成假死的参数
arg*
拿出来组成同样的命令,开启一个新的命令行手动执行,一切正常- 也验证了上一条,说明工具一切正常
继续调试,当程序假死的时候,把 external-tool
的进程强制杀死,此时 Java web 程序抛出异常, 说明刚才卡在了 p.waitFor()
这一行。
这说明通过 Runtime.exec()
开启的进程和通过命令手动执行的进程在某些地方是不一样的,最终通过搜索加测试,定位到根本原因在于:
- 每个进程都有自己对应的的输出流,包括标准输出流和错误输出流
- 如果主程序不主动接管子进程的输出流,那么子进程的输出流就会写到系统的缓冲区中
- 系统的缓冲区是有固定大小的,这也是为什么这个 BUG 中会出现每次都会卡在同一个地方,因为每运行到这个地方,系统的缓冲区就被写满了
所以解决方案是主程序要接管子进程的输出流并不断读取,防止子程序的输出缓冲区被写满,修改后的示例如下:
public static void main(String[] args) {
Process p = null;
BufferedReader br = null;
BufferedReader brError = null;
try {
p = Runtime.getRuntime().exec("external-tool arg0 arg1 arg2");
String line = null;
// read input stream
br = new BufferedReader(new InputStreamReader(p.getInputStream()));
while ((line = br.readLine()) != null) {}
// read error stream
brError = new BufferedReader(new InputStreamReader(p.getErrorStream()));
while ((line = brError.readLine()) != null) {}
int exitValue = p.waitFor();
if (exitValue != 0) {
System.out.println("Exec failed");
return;
}
System.out.println("Exec success");
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
if (p != null) {
p.destroy();
}
}
}