作者:zxcodestudy
原文:https://blog.csdn.net/qq_16681169/article/details/94592472
事件背景
我在鳳巢團隊獨立搭建和運維的一個高流量的推廣實況系統,是通過HttpClient 調用大搜的實況服務。最近經常出現Address already in use (Bind failed)的問題。很明顯是一個埠綁定衝突的問題,於是大概排查了一下當前系統的網絡連接情況和埠使用情況,發現是有大量time_wait的連接一直占用著埠沒釋放,導致埠被占滿(最高的時候6w+個),因此HttpClient建立連接的時候會出現申請埠衝突的情況。
具體情況如下:
於是為了解決time_wait的問題,網上搜索了些許資料加上自己的思考,於是認為可以通過連接池來保存tcp連接,減少HttpClient在並發情況下隨機打開的埠數量,復用原來有效的連接。但是新的問題也由連接池的設置引入了。
問題過程
在估算連接池最大連接數的時候,參考了業務高峰期時的請求量為1分鐘1.2w pv,接口平響為1.3s(複雜的廣告推廣效果模擬系統,在這種場景平響高是業務所需的原因),因此qps為12000*1.3\\60=260
然後通過觀察了業務日誌,每次連接建立耗時1.1s左右, 再留70%+的上浮空間(怕連接數設置小出系統故障),最大連接數估計為260*1.1*1.7約等於500
為了減少對之前業務代碼最小的改動,保證優化的快速上線驗證,仍然使用的是HttpClient3.1 的MultiThreadedHttpConnectionManager,設置核心代碼如下:
然後在線下手寫了多線程的測試用例,測試了下並發度確實能比沒用線程池的時候更高,然後先在我們的南京機房小流量上線驗證效果,效果也符合預期之後,就開始整個北京機房的轉全。結果轉全之後就出現了意料之外的系統異常。。。
案情回顧
在當天晚上流量轉全之後,一起情況符合預期,但是到了第二天早上就看到用戶群和相關的運維群里有一些人在反饋實況頁面打不開了。這個時候我在路上,讓值班人幫忙先看了下大概的情況,定位到了耗時最高的部分正是通過連接池調用後端服務的部分,於是可以把這個突發問題的排查思路大致定在圍繞線程池的故障來考慮了。
於是等我到了公司,首先觀察了一下應用整體的情況:
深入排查
由於發現了有近 1/3的實例進程崩潰,而業務流量沒變,由於RPC服務對provider的流量進行負載均衡,所以引發單台機器的流量升高,這樣會導致後面的存活實例更容易出現崩潰問題,於是高優看了進程掛死的原因。由於很可能是修改了HttpClient連接方式為連接池引發的問題,最容易引起變化的肯定是線程和CPU狀態,於是立即排查了線程數和CPU的狀態是否正常。
CPU狀態
如圖可見Java進程占用cpu非常高,是平時的近10倍。
線程數監控狀態
圖中可以看到多個機器大概在10點初時,出現了線程數大量飆升,甚至超出了虛擬化平台對容器的2000線程數限制(平台為了避免機器上的部分容器線程數過高,導致機器整體夯死而設置的熔斷保護),因此實例是被虛擬化平台kill了。之前為什麼之前在南京機房小流量上線的時候沒出現線程數超限的問題,應該和南京機房流量較少,只有北京機房流量的1/3有關。
接下來就是分析線程數為啥會快速積累直至超限了。這個時候我就在考慮是否是連接池設置的最大連接數有問題,限制了系統連接線程的並發度。為了更好的排查問題,我回滾了線上一部分的實例,於是觀察了下線上實例的 tcp連接情況和回滾之後的連接情況。
回滾之前tcp連接情況
回滾之後tcp連接情況
發現連接線程的並發度果然小很多了,這個時候要再確認一下是否是連接池設置導致的原因,於是將沒回滾的機器進行jstack了,對Java進程中分配的子線程進行了分析,終於可以確認問題。
jstack狀態
從jstack的日誌中可以很容易分析出來,有大量的線程在等待獲取連接池裡的連接而進行排隊,因此導致了線程堆積,因此平響上升。由於線程堆積越多,系統資源占用越厲害,接口平響也會因此升高,更加劇了線程的堆積,因此很容易出現惡性循環而導致線程數超限。
那麼為什麼會出現並發度設置過小呢?之前已經留了70%的上浮空間來估算並發度,這裡面必定有蹊蹺!
於是我對源碼進行了解讀分析,發現了端倪:
如MultiThreadedHttpConnectionManager源碼可見,連接池在分配連接時調用的doGetConnection方法時,對能否獲得連接,不僅會對我設置的參數maxTotalConnections進行是否超限校驗,還會對maxHostConnections進行是否超限的校驗。
於是我立刻網上搜索了下maxHostConnections的含義:每個host路由的默認最大連接,需要通過setDefaultMaxConnectionsPerHost來設置,否則默認值是2。
所以並不是我對業務的最大連接數計算失誤,而是因為不知道要設置DefaultMaxConnectionsPerHost而導致每個請求的Host並發連接數只有2,限制了線程獲取連接的並發度(所以難怪剛才觀察tcp並發度的時候發現只有2個連接建立 )
案情總結
到此這次雪崩事件的根本問題已徹底定位,讓我們再次精鍊的總結一下這個案件的全過程:
關於優化方案與如何避免此類問題再次發生,我想到的方案有3個:
以下是我設計的一個壓測方案:
綜上所述,一次連接池參數導致的雪崩問題已經從分析到定位已全部解決。在技術改造時我們應該要謹慎對待升級的技術點。在出現問題後,要重點分析問題的特徵和規律,找到共性去揪出根本原因。