作者:zxcodestudy 來源: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,設定核心代碼如下:
public void init() {
connectionManager = new MultiThreadedHttpConnectionManager();
HttpConnectionManagerParams managerParams = new HttpConnectionManagerParams();
managerParams.setMaxTotalConnections(500); // 最大連接配接數
connectionManager.setParams(managerParams);
client = new HttpClient(connectionManager);
}
然後線上下手寫了多線程的測試用例,測試了下并發度确實能比沒用線程池的時候更高,然後先在我們的南京機房小流量上線驗證效果,效果也符合預期之後,就開始整個北京機房的轉全。結果轉全之後就出現了意料之外的系統異常。。。 案情回顧 在當天晚上流量轉全之後,一起情況符合預期,但是到了第二天早上就看到使用者群和相關的運維群裡有一些人在回報實況頁面打不開了。 這個時候我在路上,讓值班人幫忙先看了下大概的情況,定位到了耗時最高的部分正是通過連接配接池調用後端服務的部分,于是可以把這個突發問題的排查思路大緻定在圍繞線程池的故障來考慮了。 于是等我到了公司,首先觀察了一下應用整體的情況:
- 監控平台的業務流量表現正常,但是部分機器的網卡流量略有突增
- 接口的平響出現了明顯的上升
- 業務日志無明顯的異常,不是底層服務逾時的原因,是以平響的原因肯定不是業務本身
- 發現30個機器執行個體竟然有9個出現了挂死的現象,其中6個北京執行個體,3個南京執行個體
深入排查 由于發現了有近 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個連接配接建立 ? ) 案情總結 到此這次雪崩事件的根本問題已徹底定位,讓我們再次精煉的總結一下這個案件的全過程:
- 連接配接池設定錯參數,導緻最大連接配接數為2
- 大量請求線程需要等待連接配接池釋放連接配接,出現排隊堆積
- 夯住的線程變多,接口平響升高,占用了更多的系統資源,會加劇接口的耗時增加和線程堆積
- 最後直至線程超限,執行個體被虛拟化平台kill
- 部分執行個體挂死,導緻流量轉移到其他存活執行個體。其他執行個體流量壓力變大,容易引發雪崩。
關于優化方案與如何避免此類問題再次發生,我想到的方案有3個:
- 在做技術更新前,要仔細熟讀相關的官方技術文檔,最好不要遺漏任何細節
- 可以在網上找其他可靠的開源項目,看看别人的優秀的項目是怎麼使用的。比如github上就可以搜尋技術關鍵字,找到同樣使用了這個技術的開源項目。要注意挑選品質高的項目進行參考
- 先線上下壓測,用控制變量法對比各類設定的不同情況,這樣把所有問題線上下提前暴露了,再上線心裡就有底了
以下是我設計的一個壓測方案:
- 測試不用連接配接池和使用連接配接池時,分析整體能承受的qps峰值和線程數變化
- 對比setDefaultMaxConnectionsPerHost設定和不設定時,分析整體能承受的qps峰值和線程數變化
- 對比調整setMaxTotalConnections,setDefaultMaxConnectionsPerHost 的門檻值,分析整體能承受的qps峰值和線程數變化
- 重點關注壓測時執行個體的線程數,cpu使用率,tcp連接配接數,端口使用情況,記憶體使用率
綜上所述,一次連接配接池參數導緻的雪崩問題已經從分析到定位已全部解決。在技術改造時我們應該要謹慎對待更新的技術點。在出現問題後,要重點分析問題的特征和規律,找到共性去揪出根本原因。
(完)
最近熱文: 分享一份Java架構師學習資料! 一次性搞定 Nginx 限流,問你怕不怕! 面試必考:TCP 的三次握手與四次揮手 為什麼大家都說SELECT * 效率低? 一百多道難搞的面試題,你能答對了多少? 又臭又長!流着淚我也要把它給改完! 放棄Hibernate、JPA、Mybatis! 都說了多少遍,不要再學 JSP 了!
——長按關注Java大後端——
戳原文,擷取一份面試題資料!