JAVA中外部进程输出流处理误区

Posted by Yuanyeex on Wednesday, March 22, 2017

在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则不会阻塞。

这个问题的解决有两种方法:

  1. 在我们这个场景中,可以不用显示的去关闭输出流。我们在掉process.destory()时error stream也会被关闭
  2. 采用类似EPOLL轮训策略,在调用阻塞的read()方法钱,调用非阻塞的available()方法,该方法会得到流总可读的字节数(这个结果不精确),确保流中有数据了再调用阻塞的read()方法。

我们选取的是方案2。

经过这么一次折腾,还是有这么一些启发和思考:

  1. 对于程序内启动的外部程序,一定要记得消耗外部进程的标准输出流,切记还有标准错误流(有些三方库会利用标准错误流打日志啊!!!!)
  2. 开发过程中,关键路径的日志很重要,遇到问题时,日志是定位问题的一大利器,对快速分析和定位有很大的帮助
  3. 最后嘛,windows系统是一个让程序员又爱又恨的系统。好在现在大部分的服务都是泡在linux平台上

「真诚赞赏,手留余香」

Yuanyeex

真诚赞赏,手留余香

使用微信扫描二维码完成支付