
?? 目錄
- ?? 一天上漲五十個線程?
- ?? 初步分析
- ?? 復現(xiàn)方式
- ?? 二分法確定問題
- ??? 總結
?? 一天上漲五十個線程?
上次提到,經(jīng)過初步修復后,又繼續(xù)觀察了幾天線程變化情況,發(fā)現(xiàn)每天仍然會有 50 個線程的泄露
雖然對比之前已經(jīng)好了太多,但是現(xiàn)在的情況仍然難以接受,問題沒有徹底解決,因此又開始了新一輪的排查與修復
?? 初步分析
確認存在問題以后,首先要確認泄露的線程是哪些
每隔一段時間就搜集一下線程數(shù)據(jù) ( 方式參考 一個 Jenkins 實例啟動了兩萬多個線程? ),通過對比前后兩次線程詳情,確認了泄露的線程如下

這種線程是最讓人反感的,沒有任何調(diào)用方的提示,壓根找不到在哪里發(fā)生了泄露,讓人垂頭喪氣
但是看線程變化趨勢,即使是在 Jenkins 靜息狀態(tài)(沒有流水線執(zhí)行)下,線程也會增長,看起來泄露和執(zhí)行流水線關系不大
聯(lián)系到在 上篇文章 中提到的 kubernetes-client/java 存在的線程泄露的問題,懷疑這次是否又是類似的問題?
瀏覽了 controller 相關的代碼。發(fā)現(xiàn)在 controller 啟動時,會啟動一個 informer ,這個 informer 也會啟動一個線程,public SharedInformerFactory() 在初始化 SharedInformerFactory 時會通過 Executors.newCachedThreadPool() 新建一個線程池
因為暫時沒找到哪里會關閉這個 informer,所以懷疑是這里的問題,通過將 informer 代碼的調(diào)用注釋掉,發(fā)現(xiàn)線程仍然會繼續(xù)增長,明顯不是這里造成的問題,后續(xù)才發(fā)現(xiàn) informer 的關閉是在 ControllerManager shutdown() 中完成的
線索斷掉了,單純的分析代碼可能難有進展,那就分析下線程變化,看看能夠有什么進展
?? 復現(xiàn)方式
耐心的分析線程變化,發(fā)現(xiàn)線程泄露也是周期性的,每次會新增兩個線程,那可能和上一個 TIMED_WAITTING 線程泄露的觸發(fā)條件一樣
因此使用一些手段重啟了 controller manager ,發(fā)現(xiàn)線程的泄露數(shù)目會增加的很快,之前是周期性的增長,這次在重啟 controller manager 之后,可以明顯看到類似線程的泄露現(xiàn)象
這就算是找到了穩(wěn)定的復現(xiàn)方式了,問題有解決的希望了
?? 二分法確定問題
線程堆棧信息中沒有提供指向性明確的信息,因為目前只是確認了問題代碼范圍,就只能用原始的二分法來確定問題代碼了
首先將 controller manager 的調(diào)用代碼注釋掉,編譯插件部署后,發(fā)現(xiàn)問題果然不在復現(xiàn)了,因此基本能夠確認問題就在 controller manager 這里
后續(xù)將 controller 分為兩組,一部分啟動,一部分不啟動,編譯部署,通過復現(xiàn)方式確認線程變化后,可以找到在哪些 controller 中存在問題
找到對應的 controller 后,再將 controller 的代碼分為上下兩部分,注釋一部分,編譯復現(xiàn)確認,再注釋一部分,編譯復現(xiàn)確認,最終終于發(fā)現(xiàn)了問題代碼
問題就出現(xiàn)在下面這段代碼中,在某些 controller 啟動時,會調(diào)用這里的 pollWithNoInitialDelay 函數(shù),這個函數(shù)啟動時就會初始化一個線程,用來完成任務,但是這里缺少關閉這個線程的邏輯

目前已經(jīng)知道,每小時 controller manger 會重啟一次,也就是意味著這里每天會重啟 24 次,看 usage 也能知道有兩個地方在調(diào)用這個函數(shù),所以次重啟會泄露兩個線程,計算下來正好是 48 個線程,正好與之前問題復現(xiàn)中提到的 50 個線程匹配
看起來或許這個就是最后一個窟窿了
問題足夠簡單,修復也很簡單,只需要在 finally 處關閉這個線程即可,這樣無論該函數(shù)調(diào)度多少次,都不會再發(fā)生線程泄露了
try {
while (System.currentTimeMillis() < dueDate) {
if (result.get()) {
future.cancel(true);
return true;
}
}
} catch (Exception e) {
return result.get();
} finally {
# 將線程關閉
executorService.shutdown();
}
后續(xù)連續(xù)觀察了三天線程變化,并且在這期間跑了幾次集成測試,通過在 Jenkins 上調(diào)度了很多流水線模擬正常使用情況
發(fā)現(xiàn)在流水線執(zhí)行過程中,線程會有變化起伏,但是都沒有超過 250 個,在流水線執(zhí)行結束后,最終都會穩(wěn)定回歸到 170 左右

后續(xù)在 Jenkins 靜息狀態(tài)下觀察了一周,發(fā)現(xiàn)線程一直在 170 上下,沒有出現(xiàn)線程泄露的情況,至此確認問題解決,所有的窟窿都被補上了
??? 總結
在 Java 中啟動線程時,一定要考慮關閉線程,我們應該要考慮照顧到這樣一個場景,也就是 這段代碼如果被反復調(diào)用一萬次,是否會出現(xiàn)線程泄露,不能單純的認為,這個初始化的代碼應該只會被調(diào)用一次,就不去關閉線程
另外在啟動和關閉線程時,考慮 debug 打印一下這兩個操作,不僅有助于自己判斷對線程的處理是否生效,也對后來可能的排錯場景大有裨益
最后對我來說,這次解決問題時最大的提升,就是在我走投無路時,耐心的查看線程變化信息,并且堅信它會提供給我解決問題的信息,最終結果也沒讓我失望,我覺得這是這次對我來說最重要的變化
希望通過這兩篇線程泄露排查的博文,能為各位在解決類似問題時,提供一些解決思路,幫助各位解決問題
有任何問題歡迎評論交流!