在JAVA中通过Runtime启动一个外部进程是一个常见的做法,但是如果外部进程的输出流没有被正确的处理,往往会带来一些意想不到的结果。最近我就遇到过一次这样的问题。
在我的程序中,有这么一个功能,启动一个外部进程,获取该外部进程的输入流和输出流。这里权且将我的程序称为主程序,在主程序中启动起来的程序称做子程序。在主程序中,我们会拿到子程序的标准输入流和标准输出流,基于这两个流,主程序和子程序进行通信(不用问我为什么不用socket,这里有一堆理由~)。为了避免子程序空闲太长时间,子程序在空闲一段时间后,调用System.exit(-1)
结束进程。
后来我们遇到什么问题呢?我们发现在某些环境上,程序跑一段时间后,CPU会发生spike。经过一些定位后,我们发现CPU的SPIKE总是发生在子程序调用System.exit(-1)
后,且子程序并没有退出,反而开始占用大量的CPU,单个进程大概占20%左右(想象我们会启动多个子进程,结果就是系统的CPU程序阶梯状的跳跃)。这时候我们顺理成章的上了jstack进行堆栈dump,通过对堆栈的分析,我们发现程序hang住在java.util.logging.LogManager
的一个shutdown hooker上,该hooker其实只做了一件事情,就是刷新标准错误流的buffer:
System.err.flush();
到这里,很明显,是因为子程序的标准错误流被遗忘了,我们虽然消耗了其标准输出流,确没有处理其错误流。所以,赶紧fix,fix的方法就是在主程序中添加一个错误流消耗线程。在关闭子程序的时候,我们会先关闭对应的错误流。
static class ErrStreamTask extends Thread {
private final InputStream is;
ErrStreamTask(InputStream is) {
this.is = is;
}
@Override
public void run() {
try {
int len = 0; byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != 0) { // may block is.close() in windows.
// handle out
}
}
catch (Throwable e) {
// log
}
}
public void close() {
try {
is.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
测试的时候发现,子进程调用System.exit(-1)
不会再hang住了,看似一切完美解决。但是运行一段时间后,发现还是有问题,同样通过jstack,发现当主进程尝试关闭子进程的时候,hang住在is.close()
调用上。通过一个简单的小程序发现,在windows上,如果当前输出流block在read()
函数时,调用输出流的close()
方法会同样阻塞!!!!而在linux则不会阻塞。
这个问题的解决有两种方法:
- 在我们这个场景中,可以不用显示的去关闭输出流。我们在掉process.destory()时error stream也会被关闭
- 采用类似EPOLL轮训策略,在调用阻塞的
read()
方法钱,调用非阻塞的available()
方法,该方法会得到流总可读的字节数(这个结果不精确),确保流中有数据了再调用阻塞的read()
方法。
我们选取的是方案2。
经过这么一次折腾,还是有这么一些启发和思考:
- 对于程序内启动的外部程序,一定要记得消耗外部进程的标准输出流,切记还有标准错误流(有些三方库会利用标准错误流打日志啊!!!!)
- 开发过程中,关键路径的日志很重要,遇到问题时,日志是定位问题的一大利器,对快速分析和定位有很大的帮助
- 最后嘛,windows系统是一个让程序员又爱又恨的系统。好在现在大部分的服务都是泡在linux平台上
「真诚赞赏,手留余香」
真诚赞赏,手留余香
使用微信扫描二维码完成支付