性能優化:線程資源回收

本文來自: PerfMa技術社區

PerfMa(笨馬網絡)官網

一、問題

模型服務平臺的排序請求出現較多超時情況,且不定時伴隨空指針異常。

二、問題發生前後的改動

召回引擎擴大了召回量,導致排序請求的item數量增加了。

三、出問題的模型

基於XGBoost預測的全排序模型。

四、項目介紹

web-rec-model:模型服務平臺。用於管理排序模型:XGBoost、TensorFlow、pmml....召回模型:item2item,key2item,vec2item....等模型的上下線、測試模型一致性、模型服務等。

五、一次排序請求流程

1、如下圖所示,一次排序請求流程包含:特徵獲取、向量獲取、數據處理及預測。以上提到的三個步驟均採用多線程並行處理,均以子任務形式執行。每個階段中間夾雜這數據處理的流程,由主線程進行處理,且每個階段的執行任務均爲超時返回,主線程等待子線程任務時,也採用超時等待的策略。(同事實現的一個樹形任務執行,超時等待的線程框架)

2、特徵數據閉環:該步驟爲異步執行,將排序計算使用到的特徵及分數,模型版本等信息記錄。後續作爲模型的訓練樣本,達到特徵閉環。 模型請求流程.png

3、一次排序請求中,特徵獲取及向量獲取爲網絡IO(IO密集型任務),超時可直接響應中斷,線程可快速返回。數據處理及模型爲計算步驟(CPU密集型任務)。

4、當前請求耗時情況:特徵與向量的獲取階段耗時均爲5-8ms,數據處理及模型預測階段耗時平均在10ms左右。

六、問題發生現象

1、首先是調用方:推薦策略平臺,監控報警排序請求的超時數量變多(調用方超時時間爲300ms),且從監控上看發現排序服務的耗時明顯變長:50ms+。正常高峯期的期望值爲50ms以下。

2、其次排序服務告警出現大量超時錯誤。 超時錯誤.png

3、第三根據錯誤信息定位到該錯誤信息來自於數據處理及模型預測階段。

4、除了超時變多以外,服務中會出現偶發性的空指針異常。

七、問題排查

1、首先解決空指針這類低級錯誤。 空指針錯誤.png

2、根據錯誤提示找到對應的代碼,此處就不粘貼代碼了,做一個簡單的代碼解釋。代碼邏輯爲:從Map<String,Object>中根據特徵key獲取特徵值進行計算。

3、疑惑點出現,首先該Map<String,Object>用於存放特徵及向量鍵值對,且key均做了空值計算兼容。特徵或者向量在查詢到空值時,會在Map<String,Object>中放入一個對應的默認值。經過反覆的代碼確認,報錯信息對應的代碼不可能出現漏放默認值的情況。

4、藉助Arthas的watch命令,監控空指針異常的入參。方便後面做模擬請求還原現場。

5、根據報錯時的信息進行模擬請求。嘗試N次,且使用不同的報錯數據進行嘗試,均未重現事故。

6、此時懷疑是多線程併發進行數據處理及預測時,發生對Map<String,Object>進行修改的動作,導致部分鍵值對丟失。

7、反覆檢查代碼,確定數據處理及預測均爲只讀動作,不會對Map<String,Object>進行任何鍵值對的刪改。

8、線索中斷,排查一度擱置。

八、豁然開朗

1、借用Arthas進行報錯觀察:使用watch命令,依靠-e參數(指定報錯觸發打印)以及-x n 參數(打印方法入參及返回值數據層數)

2、根據觀察,發現Map<String,Object>中丟失的均爲向量鍵值對。

3、找到問題:在排序請求流程圖中,在主線程進行分數歸一化時,會fork子線程異步做特徵數據進行壓縮寫入kafka。由於Map<String,Object>中存在大量的向量數據,導致保存數據過冗餘的情況。此處的做法是先去除所有的向量數據,再進行保存。

4、但是該動作是發生在數據處理及模型預測後的,爲何還會因爲Map<String,Object>中刪除鍵值對導致空指針異常呢。

5、此時懷疑是數據處理及模型預測階段,多線程任務還沒完成時,主線程已經等候超時返回了。

九、驗證想法

1、還是觀察超時日誌。 超時請求.png

空指針數據.png

2、發現請求已經返回後,纔出現空指針異常。那基本就可以驗證以上的想法了。

十、問題解決

1、翻看使用的多線程框架(同事實現),主線程超時等待子線程任務。主線程超時返回後,沒有通知子線程任務取消。所以才發生請求已返回,特徵數據異步落地後,偶發性出現晚到的空指針異常的情況。如下圖,主線程超時返回後,只取消主線程任務。 主線程超時等待.png

2、解決思路:主線程超時返回後,中斷子任務(取消子任務)。由於java的中斷機制爲軟中斷,一般是通過中斷標誌位進行線程中斷協作的。當然IO或者sleep的中斷由系統幫我們做了中斷可以快速返回。對於CPU密集型的任務,是需要使用者在合適的計算點上做標誌位判斷,確定是否已中斷結束任務。以這種協作的方式達到中斷。(此處可能有部分理解不當)

3、修改多線程框架,在主線程超時返回後,修改子線程中斷標誌位。 取消子線程.png

4、在計算流程中加入線程中斷檢查,如果被中斷則提前結束計算。 響應中斷.png

十一、效果檢查

1、修改發版後,空指針沒再出現。(其實該空指針是不影響排序結果,因爲結果已經是錯的,該異常只是附帶的蟲子而已) 2、超時請求減少,高峯期的超時數據減少三分之一,50ms+的排序請求有明顯減少。

十二、覆盤

1、主線程等待子任務的場景下,如果主線程超時返回了。需通知子線程結束執行的任務。首先,主線程返回了,表示子任務已被丟棄。繼續執行都是在做無用的計算,佔用計算機資源。也不是說佔着茅坑不拉屎,而是拉了沒人要。應該儘量減少服務器資源用在沒必要的消耗上。

2、該服務在數據處理及預測階段使用的線程池隊列爲SynchronousQueue,如果不瞭解SynchronousQueue的話可以簡單理解爲一個0長度的隊列。任務進池子時必須要有線程進行對接。與常規的BlockingQueue不同的是,任務在池子中不會堆積,對於任務的快速響應比較友好。但是也因爲如果沒有空閒的線程,則會不停創建線程直到最高線程數限制而觸發丟棄策略。在該項目問題中,由於部分子任務在主線程返回後仍然在執行。新的請求進來後,會出現沒有空閒線程的情況,導致池子創建新線程接任務。對於CPU密集型任務來說,過多的線程數對服務來說是另一種負擔,畢竟線程切換的代價還是比較大的。這就套入死循環了。(個人理解,如表述有誤,還望指正)

歡迎關注 PerfMa 社區,推薦閱讀

內存問題探微

JAVA線上故障排查套路

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章