Jenkins/Java 線程泄露排查(二)

angry-jenkins

?? 目錄

  • ?? 一天上漲五十個線程?
  • ?? 初步分析
  • ?? 復現(xiàn)方式
  • ?? 二分法確定問題
  • ??? 總結

?? 一天上漲五十個線程?

上次提到,經(jīng)過初步修復后,又繼續(xù)觀察了幾天線程變化情況,發(fā)現(xiàn)每天仍然會有 50 個線程的泄露

雖然對比之前已經(jīng)好了太多,但是現(xiàn)在的情況仍然難以接受,問題沒有徹底解決,因此又開始了新一輪的排查與修復

?? 初步分析

確認存在問題以后,首先要確認泄露的線程是哪些

每隔一段時間就搜集一下線程數(shù)據(jù) ( 方式參考 一個 Jenkins 實例啟動了兩萬多個線程? ),通過對比前后兩次線程詳情,確認了泄露的線程如下

leakage.png

這種線程是最讓人反感的,沒有任何調(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ù)啟動時就會初始化一個線程,用來完成任務,但是這里缺少關閉這個線程的邏輯

cause-code.png

目前已經(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 左右

stable.png

后續(xù)在 Jenkins 靜息狀態(tài)下觀察了一周,發(fā)現(xiàn)線程一直在 170 上下,沒有出現(xiàn)線程泄露的情況,至此確認問題解決,所有的窟窿都被補上了

??? 總結

在 Java 中啟動線程時,一定要考慮關閉線程,我們應該要考慮照顧到這樣一個場景,也就是 這段代碼如果被反復調(diào)用一萬次,是否會出現(xiàn)線程泄露,不能單純的認為,這個初始化的代碼應該只會被調(diào)用一次,就不去關閉線程

另外在啟動和關閉線程時,考慮 debug 打印一下這兩個操作,不僅有助于自己判斷對線程的處理是否生效,也對后來可能的排錯場景大有裨益

最后對我來說,這次解決問題時最大的提升,就是在我走投無路時,耐心的查看線程變化信息,并且堅信它會提供給我解決問題的信息,最終結果也沒讓我失望,我覺得這是這次對我來說最重要的變化


希望通過這兩篇線程泄露排查的博文,能為各位在解決類似問題時,提供一些解決思路,幫助各位解決問題

有任何問題歡迎評論交流!

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容