實例分享:記一次故障引起的線程池使用的思考

1、懸案

某日某晚 8 時許,一陣急促的報警電話響徹有贊分銷員技術團隊的工位,小虎同窗,小峯同窗紛紛打開監控平臺一探究竟。分銷員系統某核心應用,接口響應所有超時,dubbo 線程池被所有佔滿,並堆積了大量待處理任務,整個應用沒法響應任何外部請求,處於「夯死」的狀態。面試

正當虎峯兩位同窗焦急的以各類姿式查看應用的各項指標時,5分鐘過去了,應用竟然本身自動恢復了。看似虛驚一場,但果然如此嗎?spring

2、勘查線索

2.1 QPS

「是否是又有商家沒有備案就搞活動了啊」,小虎同窗如此說道。的確,對於應用忽然夯死,你們可能第一時間想到的就是流量突增。流量突增會給應用穩定性帶來不小衝擊,機器資源的消耗的增長至殆盡,就像咱們去自助餐廳胡吃海喝到最後一口水都喝不下,固然也就響應不了新的請求。咱們查看了 QPS 的情況。併發

事實讓人失望,應用的 QPS 指標並無出現陡峯,處於一個相對平緩的上下浮動的狀態,小虎同窗不由一口嘆氣,看來不是流量突增致使的.框架

2.2 GC

「是否是 GC 出問題了」,框架組一位資深的同窗說道。JVM 在 GC 時,會由於 Stop The World 的出現,致使整個應用產生短暫的停頓時間。若是 JVM 頻繁的發生 Stop The World,或者停頓時間較長,會必定程度的影響應用處理請求的能力。可是咱們查看了 GC 日誌,並無任何的異常,看來也不是 GC 異常致使的。運維

2.3 慢查

「是否是有慢查致使整個應用拖慢?」,DBA 同窗提出了本身的見解。當應用的高 QPS 接口出現慢查時,會致使處理請求的線程池中(dubbo 線程池),大量堆積處理慢查的線程,佔用線程池資源,使新的請求線程處於線程池隊列末端的等待狀態,狀況惡劣時,請求得不到及時響應,引起超時。但遺憾的是,出問題的時間段,並未發生慢查。工具

2.4 TIMEDOUT

問題至此已經撲朔迷離了,可是咱們的開發同窗並無放棄。仔細的小峯同窗在排查機器日誌時,發現了一個異常現象,某個平時不怎麼報錯的接口,在1秒內被外部調用了 500 屢次,此後在那個時間段內,根據 traceid 這 500 屢次請求產生了 400 多條錯誤日誌,而且錯誤日誌最長有延後好幾分鐘的。ui

這是怎麼回事呢?這裏有兩個問題讓咱們迷惑不解:線程

  1. 500 QPS 徹底在這個接口承受範圍內,壓力還不夠。3d

  2. 爲何產生的錯誤日誌可以被延後好幾分鐘。調試

日誌中明顯的指出,這個 http 請求 Read timed out。http 請求中讀超時設置過長的話,最終的效果會和慢查同樣,致使線程長時間佔用線程池資源(dubbo 線程池),簡言之,老的出不去,新的進不來。帶着疑問,咱們翻到了代碼。

可是代碼中確實是設置了讀超時的,那麼延後的錯誤日誌是怎麼來的呢?咱們已經接近真相了嗎?

3、破案

咱們難免對這個 RestTemplateBuilder 起了疑心,是這個傢伙有什麼暗藏的設置嘛?機智的小虎同窗,針對這個工具類,將線上的狀況回放到本地進行了模擬。咱們構建了 500 個線程同時使用這個工具類去請求一個 http 接口,這個 http 接口讓每一個請求都等待 2 秒後再返回,具體的作法很簡單就是 Thread.sleep(2000),而後觀察每次請求的 response 和 rt。

咱們發現 response 都是正常返回的(沒有觸發 Read timed out),rt是規律的5個一組而且有 2 秒的遞增。看到這裏,你們是否是感受到了什麼?對!這裏面有隊列!經過繼續跟蹤代碼,咱們找到了「元兇」。

這個工具類默認使用了隊列去發起 http 請求,造成了相似 pool 的方式,而且 pool active size 僅有 5

如今咱們來還原下整個案件的通過:

  1. 500 個併發的請求同時訪問了咱們應用的某個接口,將 dubbo 線程池迅速佔滿(dubbo 線程池大小爲 200),這個接口內部邏輯須要訪問一個內網的 http 接口

  2. 因爲某些不可抗拒因素(運維同窗還在辛苦奮戰),這個時間段內這個內網的 http 接口所有返回超時

  3. 這個接口發起 http 請求時,使用隊列造成了相似 pool 的方式,而且 pool active size 僅有 5,因此消耗完 500 個請求大約須要 500/5*2s=200s,這 200s 內應用自己承擔着大約 3000QPS 的請求,會有大約 3000*200=600000 個任務會進入 dubbo 線程池隊列(如懸案中的日誌截圖)。PS:整個應用固然就涼涼咯。

  4. 消耗完這 500 個請求後,應用就開始慢慢恢復(恢復的速率與時間能夠根據正常 rt 大體算一算,這裏就不做展開了)。

4、思考

到這裏,你們內心的一塊石頭已經落地。但回顧整個案件,無非就是咱們工做中或者面試中,常常碰到或被問到的一個問題:「對象池是怎麼用的呢?線程池是怎麼用的呢?隊列又是怎麼用的呢?它們的核心參數是怎麼設置的呢?」。答案是沒有標準答案,核心參數的設置,必定須要根據場景來。拿本案舉例,本案涉及兩個方面:

4.1 發起 http 請求的隊列

這個使用隊列造成的 pool 的場景是側重 IO 的操做IO 操做的一個特性是須要有較長的等待時間,那咱們就能夠爲了提升吞吐量,而適當的調大 pool active size(反正你們就一塊兒等等咯),這和線程池的的 maximum pool size 有着殊途同歸之處。那調大至多少合適呢?能夠根據這個接口調用狀況,平均 QPS 是多少,峯值 QPS 是多少,rt 是多少等等元素,來調出一個合適的值,這必定是一個過程,而不是一次性決定的。那又有同窗會問了,我是一個新接口,我不知道歷史數據怎麼辦呢?對於這種狀況,若是條件容許的話,使用壓測是一個不錯的辦法。根據改變壓測條件,來調試出一個相對靠譜的值,上線後對其觀察,再決定是否須要調整。

4.2 dubbo 線程池

在本案中,對於這個線程池的問題有兩個,隊列長度與拒絕策略。隊列長度的問題顯而易見,一個應用的負載能力,是能夠經過各類手段衡量出來的。就像咱們去餐廳吃飯同樣,顧客從上桌到下桌的平均時間(rt)是已知的,餐廳一天存儲的食物也是已知的(機器資源)。當餐桌滿了的時候,新來的客人須要排隊,若是不限制隊列的長度,一個餐廳外面排上個幾萬人,隊列末尾的老哥好不容易輪到了他,但他已經餓死了或者餐廳已經沒吃的了。這個時候,咱們就須要學會拒絕。能夠告訴新來的客人,大家今天晚上是排不上的,去別家吧。也能夠把那些吃完了,可是賴在餐桌上聊天的客人趕趕走(雖然現實中這麼挺不禮貌,但也是一些自助餐限時2小時的緣由)。回到本案,若是咱們調低了隊列的長度,增長了適當的拒絕策略,而且能夠把長時間排隊的任務移除掉(這麼作有必定風險),能夠必定程度的提升系統恢復的速度

最後補一句,咱們在使用一些第三方工具包的時候(就算它是 spring 的),須要瞭解其大體的實現,避免因參數設置不全,帶來意外的「收穫」。

相關文章
相關標籤/搜索