關於Runtime.getRuntime().exec()產生阻塞的2個陷阱


本文來自網易雲社區


背景

相信做java服務端開發的童鞋,經常會遇到Java應用調用外部命令啟動一些新進程來執行一些操作的場景,這時候就會使用到Runtime.getRuntime().exec(),然而這個方法如果不謹慎很容易掉進陷阱。

我們的一個PDF轉碼服務就踩到了這個坑掉進陷阱,這個轉碼服務主要是對pdf進行加密和轉碼成swf。這個服務上線后大部分時間都是穩定運行的,但是隔一段時間就會死掉,然后人肉手動重啟一下服務就復活了。看了日志,有時候有一堆關於pdf轉碼過程的錯誤日志,有時候死掉的時候什么日志也沒輸出。這時候猜測可能是pdf轉碼異常導致應用掛掉的{因為這個轉碼服務一直是單線程在工作}。更深的原因大家也空沒去找。反正運營反饋上傳的pdf一直處在轉碼中很久了,一兩天了還在轉碼中,於是開發就手動重啟下服務。是的你沒看過,就是一兩天才發現,我們的業務監控沒作上去,因為相對迭代任務,這都算不緊急的事情了。

后來運營反饋pdf問題次數增多了,於是寫了個腳本,定時去檢查日志最后的更新時間,發現日志超過一個小時沒更新就重啟應用,重啟腳本沒問題,問題是應用重啟后,日志中出現了一堆的找不到要執行的命令。目前也不知道為什么通過腳本去重啟動應用后,應用找不到要執行的命令。有知道的可以告知下。

終於某一天,應用又死掉了,看了下數據庫堆積了將近2000個待轉的文件。看了下應用日志打了exe()后就再也沒內容了,於是下狠心花了半天時間來研究下Runtime.getRuntime().exe()找了下原因,最終解決了這個問題。


關於Runtime.getRuntime().exe()

根據jdk官方文檔描述,每個Java應用都存在一個而Runtime的單例實例。這個類Runtime類封裝了應用運行時的環境,通過這個類我們的java應用可以與其運行環境相連接。

1、java應用無法創建自己的Runtime實例,只能通過Runtime.getRuntime()來取得當前JVM的運行時環境,這也是在Java中唯一一個得到運行時環境的方法。一旦得到了一個當前的Runtime對象的引用,就可以調用Runtime對象的方法來控制Java虛擬機的狀態和行為。

2、Runtime中的exit方法是退出當前JVM的方法,System類中的exit實際上也是通過調用Runtime.exit()來退出JVM的,這里說明一下Java對Runtime返回值的一般規則(后邊也提到了),0代表正常退出,非0代表異常中止。

3、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對象除了可以多的三種輸入輸出流外,還有兩個常用的方法:

1、非阻塞方法exitValue()獲得子進程退出的狀態值(0,正常退出,非0異常退出),需要注意的是調用這個方法程序會立即得到結果,如果子進程沒有執行完,調用這個方法會拋出IllegalThreadStateException,表示此 Process 對象表示的子進程尚未終止。

2、阻塞方法 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

https://segmentfault.com/a/1190000000372535





網易雲產品免費體驗館,無套路試用,零成本體驗雲計算價值。  

本文來自網易實踐者社區,經作者潘勝一授權發布



相關文章:
【推薦】 如何通俗地解釋雲計算,看完這組圖就明白了
【推薦】 搜索實時個性化模型——基於FTRL和個性化推薦的搜索排序優化
【推薦】 網易易盾驗證碼的安全策略


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM