-
旨在分享工作中遇到的各種問題及解決思路與方案,與大家一起學習. -- 學無止境, 加油 ! Just do it !
-
問題描述
-
運行環境描述
-
tomcat-8.5
-
單節點(該應用集群20個節點) avg-tps 250,max-tps 350
-
tomcat max-threads:200 (下圖藍色線)
-
tomcat busy-threads 正常(下圖綠色線)
-
tomcat cur-threads飛升(下圖黃色線)
-
每次黃色線上升時可以發現原本平均響應時間100ms內的接口響應時間均在3-10s

-
-
提出問題
使用grafana監控發現服務某個節點的cur線程數會暴漲直至Max-threads數且一直無法回收
-
期望
解決cur-threads回收問題,讓線程正常回收
-
-
原因分析
-
線程問題首先來一波jstack
上圖是當時某個節點線程飆升時dump下來的線程日志,在這個時間點的線程中有大量的TIMED_WAITING 狀態,可以先復習一波線程狀態了,走起. -
Java線程的5種狀態
-
新建狀態(New): 線程對象被創建后,就進入了新建狀態。例如,Thread thread = new Thread()。
-
就緒狀態(Runnable): 也被稱為“可執行狀態”。線程對象被創建后,其它線程調用了該對象的start()方法,從而來啟動該線程。例如,thread.start()。處於就緒狀態的線程,隨時可能被CPU調度執行。
-
運行狀態(Running): 線程獲取CPU權限進行執行。需要注意的是,線程只能從就緒狀態進入到運行狀態。
-
阻塞狀態(Blocked): 阻塞狀態是線程因為某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,才有機會轉到運行狀態。阻塞的情況分三種:
-
等待阻塞 -- 通過調用線程的wait()方法,讓線程等待某工作的完成。
-
同步阻塞 -- 線程在獲取synchronized同步鎖失敗(因為鎖被其它線程所占用),它會進入同步阻塞狀態。
-
其他阻塞 -- 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
-
-
死亡狀態(Dead): 線程執行完了或者因異常退出了run()方法,該線程結束生命周期。

-
-
Jstack中常見的線程狀態
-
RUNNABLE 線程運行中或I/O等待
-
BLOCKED 線程在等待monitor鎖(synchronized關鍵字)
-
TIMED_WAITING 線程在等待喚醒,但設置了時限(lock.wait(10))
-
WAITING 線程在無限等待喚醒(lock.wait(10))
-
復習完了,結合上面的線程日志以及服務中高並發的接口,找到有用到lock鎖的接口,分析代碼,到這一步基本算是找到解題思路了,如此多的線程等待是因為並發的查詢接口緩存穿透了 接下來還要dump下這個節點的堆內存來具體分析,准確定位,下圖是堆內存日志:
很明顯可以看到堆中的大對象內容,結合實際業務可以准確定位需要優化的接口了,那么cur-threads線程數為什么一直增長呢?為什么不回收呢?帶着這兩個疑問,我們先去找下tomcat官網針對這兩個參數的描述; 
-
-
上圖可以看到最大線程數默認是200,初始化空閑線程數10,與我們線上環境一致(附上圖中
-
上圖也是找的tomcat官網(附上圖中
-
總結
-
cur-threads一直增長的原因
-
接口並發且發生了大量緩存穿透(線程日志中大量time_wait線程是項目中防緩存穿透使用的鎖),造成鎖等待,進而造成tomcat當前線程不夠用,所以cur線程數據增加,每次在線程數增加的時候接口響應均達到秒級別,可能創建Thread比較消耗資源,這塊有待驗證!
-
-
tomcat線程一直不回收的原因
-
Tomcat線程池每次從隊列頭部取線程去處理請求,請求完結束后再放到隊列尾部,在高並發下,每個線程都會在短時間內被使用,達不到1分鍾空閑被回收的條件
-
-
-
解決方案與建議
-
需要優化響應慢的接口(治本)
-
如果可以,降低接口並發(治標)
-
適當增加tomcat的maxThreads值可以提升應用性能(不是越大越好,最優配置數值需要模擬pro環境經過大量壓測對比得出)
-
-
優化后

-
本次改造有兩個點
-
降低並發(比如serv A->serv-B,查詢並發比較高,可以根據實際業務考慮在A系統做緩存,降低B系統並發)
-
優化響應慢的接口 (如果業務復雜可以先考慮設計是否合理再考慮技術改造(多線程,緩存中間件))
-
-
歡迎關注個人訂閱號:Java技術寶典 ,及時獲取最新分享. 

