关于Runtime.getRuntime().exec()产生阻塞的2个陷阱

本文来自网易云社区html


背景java

相信作java服务端开发的童鞋,常常会遇到Java应用调用外部命令启动一些新进程来执行一些操做的场景,这时候就会使用到Runtime.getRuntime().exec(),然而这个方法若是不谨慎很容易掉进陷阱。web

咱们的一个PDF转码服务就踩到了这个坑掉进陷阱,这个转码服务主要是对pdf进行加密和转码成swf。这个服务上线后大部分时间都是稳定运行的,可是隔一段时间就会死掉,而后人肉手动重启一下服务就复活了。看了日志,有时候有一堆关于pdf转码过程的错误日志,有时候死掉的时候什么日志也没输出。这时候猜想多是pdf转码异常致使应用挂掉的{由于这个转码服务一直是单线程在工做}。更深的缘由你们也空没去找。反正运营反馈上传的pdf一直处在转码中好久了,一两天了还在转码中,因而开发就手动重启下服务。是的你没看过,就是一两天才发现,咱们的业务监控没做上去,由于相对迭代任务,这都算不紧急的事情了。shell

后来运营反馈pdf问题次数增多了,因而写了个脚本,定时去检查日志最后的更新时间,发现日志超过一个小时没更新就重启应用,重启脚本没问题,问题是应用重启后,日志中出现了一堆的找不到要执行的命令。目前也不知道为何经过脚本去重启动应用后,应用找不到要执行的命令。有知道的能够告知下。数据库

终于某一天,应用又死掉了,看了下数据库堆积了将近2000个待转的文件。看了下应用日志打了exe()后就再也没内容了,因而下狠心花了半天时间来研究下Runtime.getRuntime().exe()找了下缘由,最终解决了这个问题。segmentfault


关于Runtime.getRuntime().exe()api

根据jdk官方文档描述,每一个Java应用都存在一个而Runtime的单例实例。这个类Runtime类封装了应用运行时的环境,经过这个类咱们的java应用能够与其运行环境相链接。缓存

一、java应用没法建立本身的Runtime实例,只能经过Runtime.getRuntime()来取得当前JVM的运行时环境,这也是在Java中惟一一个获得运行时环境的方法。一旦获得了一个当前的Runtime对象的引用,就能够调用Runtime对象的方法来控制Java虚拟机的状态和行为。安全

二、Runtime中的exit方法是退出当前JVM的方法,System类中的exit实际上也是经过调用Runtime.exit()来退出JVM的,这里说明一下Java对Runtime返回值的通常规则(后边也提到了),0表明正常退出,非0表明异常停止。oracle

三、Runtime具备的详细方法请参考官方api,http://docs.oracle.com/javase/8/docs/api/


阻塞陷阱之Runtime.getRuntime().exe()的返回值Process

应用在调用Runtime.getRuntime().exec()这个方法会建立一个本机进程并返回Process子类的一个实例。该实例可用来控制该进程并得到其相关信息。Process类提供了执行从进程输入、执行输出到进程、等待进程完成、检查进程的退出状态以及销毁(杀掉)进程的方法。

官方文档解释了建立进程的方法可能没法针对某些本机平台上的特定进程很好地工做,好比,本机窗口进程,守护进程,Microsoft Windows 上的 Win16/DOS 进程,或者 shell脚本。建立的子进程没有本身的终端或控制台。它的全部标准 io(即 stdin、stdout 和 stderr)操做都将经过三个管道重定向到父进程(也就是调用者java应用)。三个管道用于处理标准输入流,标准输出流,标准错误流。子进程在执行过程当中,会不断的向JVM写入标准输出和标准错误输出。java应用能够经过Process 提供的getOutputStream()、getInputStream() 和 getErrorStream()来得到子进程输入输出信息。由于有些本机平台仅针对标准输入和输出流提供有限的缓冲区大小,当标准输出或者标准错误输出写满缓存池时,程序没法继续写入,子进程没法正常退出。读写子进程的输出流或输入流迅速出现失败,则可能致使子进程阻塞,甚至产生死锁。

当调用Runtime.getRuntime().exe()后返回的Process对象除了能够多的三种输入输出流外,还有两个经常使用的方法:

一、非阻塞方法exitValue()得到子进程退出的状态值(0,正常退出,非0异常退出),须要注意的是调用这个方法程序会当即获得结果,若是子进程没有执行完,调用这个方法会抛出IllegalThreadStateException,表示此 Process 对象表示的子进程还没有终止。

二、阻塞方法 waitFor()致使当前线程等待,直到子进程结束并返回退出状态。若是已终止该子进程,此方法当即返回,若是没有终止该子进程,调用的线程将被阻塞,直到退出子进程。

   先看看咱们转码服务这里的历史代码:

   

这段代码,用同步的方法去读取标准错误输出流即至关于清空了错误输出流缓冲区,然而正常的标准输出流并无清空,按照上面的原理解释,阻塞的缘由可能就产生在这里。当阻塞产生的时候jstack了一下线程栈信息以下图所示。确实线程锁在了读取缓冲流上面了。

这种状况网上通用的解决方法就是异步开两个线程去读取正常的输出和错误输出流信息,清空缓冲区,参考了你们的解决方法,下图是修改后的方案,ProcessClearStream是一个异步线程,主要作的是将标准inputSream读取完毕。


阻塞陷阱之子进程阻塞

经过上面的代码优化后仍是发现有转码阻塞的现象出现,并且发现每次阻塞都出如今固定的几个pdf上,测试发现重启应用后主要转到那几个特定的pdf时候,转码服务必挂无疑(一般一个pdf转码只须要几十秒,而这个阻塞持续几个小时,不人为干预它就可能无限阻塞下去)。因此重启应用也无论用了,只能跳过这几个pdf应用才行,因而在测试环境测试这几个pdf,每次阻塞的时候再jstack发现应用阻塞在proc.waitFor(),再也没其余错误信息了。查看了官方api,Process的waitFor方法自己会阻塞直到子进程正常或异常退出,到这里,应该能够推断是子进程无限阻塞下去了,致使waitFor一直阻塞中。为了验证这个推断,直接在终端kill掉这个子进程,而后再查看日志,发现转码服务又继续工做了。

有了上面的结论,一个简单的思路也就有了,我须要检测子进程状态,若是发现子进程有阻塞状态就kill掉(由于这个转码脚本比较老,要拿他的堆栈信息比较麻烦,因此kill掉是最简单直接暴力效率高的方法)。将这个想法和同事聊了下,万能的Java确定能够干这事,大概思路就启动个线程去监控process的waitFor的阻塞时间,超过设置时间,就干掉了子进程,这不是Java线程池ExecutorService类配合Future接口来干的事情么。同事按照这个思路网上找了下现成的代码,因而照着这个这个方法抄袭了一下,下面贴下关键的代码:

当waitFor超时线程中断的的时候再调用process的destroy()销毁子进程。这个方案上线后,截至目前一周多时间转码服务稳定运行,没在出现之前的服务死掉的状况。咱们业务中当检测到超时退出后就重置任务状态为失败(算是降级吧),致使这种pdf转码子进程阻塞的通常是pdf自己不太标准,而这个转码工具不能很好的兼容处理这些pdf,后面把这些有问题的pdf从新转成标准pdf上传测试便可以正常转码。

参考资料

http://www.javaworld.com/article/2071275/core-java/when-runtime-exec---won-t.html

http://www.cnblogs.com/BeautyConcurrency/p/4108196.html

http://www.javashuo.com/article/p-ocgcbaoa-ex.html





网易云产品免费体验馆,无套路试用,零成本体验云计算价值。  

本文来自网易实践者社区,经做者潘胜一受权发布



相关文章:
【推荐】 如何通俗地解释云计算,看完这组图就明白了
【推荐】 搜索实时个性化模型——基于FTRL和个性化推荐的搜索排序优化
【推荐】 网易易盾验证码的安全策略

相关文章
相关标签/搜索