1. 出故障了
由于從事it這個行業,我們需要每天應對故障和問題,因此我們可以稱之為消防員,處處奔波解決問題。不過,這次的故障范圍有點大,宿主機都打不開了。
好在監控系統留下了一些證據。
證據發現,機器的CPU、內存、文件句柄,隨著業務的增長,持續的上升…上升….,直到監控也無法將信息收集上來。
要命的是,這些宿主機上,部署了非常多的Java進程。沒別的原因,就是為了節省成本,混部了應用。當宿主機表現出整體性的異常時,就難以找到罪魁禍首。
由于遙控登錄已失效,急躁的運維人員只能選擇重啟機器,并在重新啟動后開始重啟應用程序。經過漫長的等待,所有進程都恢復正常運行,但是僅僅過了短暫的時間,宿主機就突然宕機了。
業務一直處于死翹翹的狀態,真是讓人惱火啊。也讓人心急。嘗試過幾次之后,運維崩潰了,啟動了緊急預案:回滾!
最近的上線記錄有點多,而且有開發人員私自上線部署的行為,運維蒙圈了:回滾哪些呢?還好有人腦瓜一亮,想起了還有find這個命令,那就找到最近更新的所有jar包,都給它來次回滾吧。
find /apps/deploy -mtime +3 | grep jar$
如果你不知道find這個命令,那可還真的是一場災難。還好有人知道。
把十來個jar包回滾,還好沒有碰到數據庫的schema變更,系統終于正常運行了。
2. 找原因
沒別的辦法,查日志,進行代碼審查。
為了確保代碼的質量,代碼審查的范圍應該限定在最近1周或2周內的代碼改動,因為一些功能代碼需要一定時間的沉淀,才能在線上獨放異彩。
看著滿屏的提交記錄“OK”,技術經理的臉都綠了。
“xjjdog說過,《80%的程序員,不會寫commit記錄》,我看你們是100%都不會寫”。
大家都靜悄悄的,忍著痛翻查歷史變更。經過大家的不懈努力,終于在屎山之間,找到了一些問題代碼。一個CxO親自創建的群,大家紛紛將可能出現問題的代碼扔進去。
“系統服務中斷了接近一個小時,影響非常惡劣”,CxO說,“務必把問題徹底解決掉,這個問題投資人非常關注”!
okokok,有了釘釘的助力,大家的手勢都變得整齊劃一。
3. 線程池的參數
代碼有點多,大家對問題代碼討論了老久。這句話的重寫如下: 我們檢查了一些使用并行流和嵌套在lambda表達式中的復雜代碼,并在其中特別關注了線程池的使用。
最后大家決定還是對線程池的代碼再過一遍。其中有一段是這么寫的。
RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardOldestPolicy(); ThreadPoolExecutor executor = new ThreadPoolExecutor(100,200, 60000, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(10), handler);
還別說,參數有模有樣的,甚至考慮到了拒絕策略。
Java的線程池,使得編程變的非常簡單。如果不逐一介紹,這些參數就無法被審查,如上圖所示。
-
corePoolSize:核心線程數,核心線程創建后會一直存活
-
maxPoolSize:最大線程數
-
keepAliveTime:線程空閑時間
-
workQueue:阻塞隊列
-
threadFactory:線程創建工廠
-
handler:拒絕策略
下面來介紹一下它們的關系。
如果線程數少于核心線程數時,有新任務到來,系統會創建一個新線程來處理該任務。如果當前線程數量超過核心線程數量,且阻塞隊列未滿,任務將被放入阻塞隊列中。當線程數大于核心線程數,而且阻塞隊列滿了的時候,將會創建新的線程進行服務,直到線程數到達maximumPoolSize的大小。此時,如果還有新的任務,將觸發拒絕策略。
再說一下拒絕策略。JDK內置了4種策略,其中默認的是AbortPolicy,即直接拋出異常。下面介紹其他幾種。
-
DiscardPolicy 比abort更加激進,直接丟掉任務,連異常信息都沒有
-
任務處理是由調用線程執行的,這是 CallerRunsPolicy 的實現方式。當一個Web應用的線程池資源被占滿后,新增的任務會被分配到Tomcat線程中執行。在某些情況下,這種方法可以減輕一些任務的執行壓力,但在更多情況下,它會直接阻塞主線程的運行
-
DiscardOldestPolicy 丟棄隊列最前面的任務,然后重新嘗試執行任務
這段線程池代碼是新添加的,參數設置也比較合理,沒有太大的問題。使用DiscardOldestPolicy拒絕策略是唯一可能存在的風險。當任務非常多的時候,這個拒絕策略會造成任務排隊,請求超時。
當然不能放過這種風險,說實話也是到現在為之能夠找到的最可能的風險代碼了。
“把DiscardOldestPolicy 改成默認的AbortPolicy吧,重新打包上線一下試試“。技術大牛在群里說。
4. 問題在哪里?
結果,服務灰度上線之后,宿主機不多時,就死掉了。是它的原因沒跑了,但是why?
線程池的大小 ,最小100,最大200,說什么也不過分。阻塞隊列的容量只有10,說什么也不會造成問題。你要說是這個線程池造成的原因,打死我都不信。
但是業務部門反饋,這段代碼加上就死,不加就沒事。技術大牛們抓耳撓腮百思不得其姐。
到最后,終于有人忍不住了,下載下業務的代碼打算調試一下。
當他打開Idea的時候,瞬間懵逼了,又瞬間領悟了。他終于明白了這段代碼為什么會產生問題了。
線程池,竟然是在方法里創建的!
當每一個請求到來的時候,它都會創建一個線程池,直到系統再也無法分配資源為止。
可真是霸道啊。
所有人都在關注線程池的參數是怎么設置的,但從來沒有人懷疑這段代碼所在的位置。