本文來自網易雲社區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和個性化推薦的搜索排序優化
【推薦】 網易易盾驗證碼的安全策略