在內網環境中,超時問題,網絡表示這個鍋我不背。html
筆者對於超時的理解,隨着在工做中不斷實踐,其理解也愈來愈深入,RocketMQ在生產環境遇到的超時問題,已經困擾了我將近半年,如今終於取得了比較好的成果,和你們來作一個分享。java
本次技術分享,因爲涉及到網絡等諸多筆者不太熟悉的領域,若是存在錯誤,請你們及時糾正,實現共同成長與進步。shell
一、網絡超時現象
時不時老是接到項目組反饋說生產環境MQ發送超時,客戶端相關的日誌截圖以下: 今天的故事將從張圖開始。編程
二、問題排查
2.1 初步分析
上圖中有兩條很是關鍵日誌:緩存
-
invokeSync:wait response timeout exception 網絡調用超時服務器
-
recive response,but not matched any request網絡
這條日誌很是之關鍵,表示儘管客戶端在獲取服務端返回結果時超時了,但客戶端最終仍是能收到服務端的響應結果,只是此時客戶端已經等待足夠時間後放棄處理了。多線程
關於第二條日誌,我再詳細闡述一下其運做機制,其實也是用一條連接發送多個請求的編程套路。一條長鏈接,向服務端前後發送2個請求,客戶端在收到服務端響應結果時,怎麼判斷這個響應結果對應的是哪一個請求呢?以下圖所示: 客戶端多個線程,經過一條鏈接發送了req1,req2兩個請求,但在服務端一般都是多線程處理,返回結果時可能會先收到req2的響應,那客戶端如何識別服務端返回的數據是對應哪一個請求的呢?運維
解決辦法是客戶端在發送請求以前,會爲該請求生成一個本機器惟一的請求id(requestId),而且會採用Future模式,將requestId與Future對象放入一個Map中,而後將reqestId放入請求體中,服務端在返回響應結果時將請求ID原封不動的放入到響應結果中,當客戶端收到響應時,先界面出requestId,而後從緩存中找到對應的Future對象,喚醒業務線程,將返回結構通知給調用方,完成整個通訊。tcp
故從這裏能看到,客戶端在指定時間內沒有收到服務端的請求,但最終仍是能收到,矛頭直接指向Broker端,是否是Broker有瓶頸,處理很慢致使的。
2.2 Broker端處理瓶頸分析
在個人「經驗」中,RocketMQ消息發送若是出現瓶頸,一般會返回各類各樣的Broker Busy,並且能夠經過跟蹤Broker端寫入PageCache的數據指標來判斷Broker是否遇到了瓶頸。
grep "PAGECACHERT" store.log
獲得的結果相似以下截圖:
舒適提示:上圖是我本機中的截圖,當時分析問題的時候,MQ集羣中各個Broker中這些數據,寫入PageCache的時間沒有超過100ms的。
正是因爲良好的pagecache寫入數據,根據以下粗糙的網絡交互特性,我提出將矛盾點轉移到網絡方面: 而且我還和業務方肯定,雖然消息發送返回超時,但消息是被持久化到MQ中的,消費端也能正常消費,網絡組同事雖然從理論上來講局域網不會有什麼問題,但鑑於上述現象,網絡組仍是開啓了網絡方面的排查。
舒適提示:最後證實是被我帶偏了。
2.3 網絡分析
一般網絡分析有兩種手段,netstat 與網絡抓包。
2.3.1 netstat查看Recv-Q與Send-Q
咱們能夠經過netstat重點觀察兩個指標Recv-Q、Send-Q。
-
Recv-Q tcp通道的接受緩存區
-
Send-Q
tcp通道的發送緩存區
在TCP中,Recv-Q與Send-Q的做用以下圖所示:
- 客戶端調用網絡通道,例如NIO的Channel寫入數據,數據首先是寫入到TCP的發送緩存區,若是發送發送區已滿,客戶端沒法繼續向該通道發送請求,從NIO層面調用Channel底層的write方法,會返回0,表示發送緩衝區已滿,須要註冊寫事件,待發送緩存區有空閒時再通知上層應用程序能夠發消息。
- 數據進入到發送緩存區後,接下來數據會隨網絡到達目標端,首先進入的是目標端的接收緩存區,若是與NIO掛鉤的化,通道的讀事件會繼續,應用從接收緩存區中成功讀取到字節後,會發送ACK給發送方。
- 發送方在收到ACK後,會刪除發送緩衝區中的數據,若是接收方一直不讀取數據,那發送方也沒法發送數據。
網絡同事分佈在客戶端、MQ服務器上經過每500ms採集一次netstat ,通過對採集結果進行彙總,出現以下圖所示: 從客戶端來看,客戶端的Recv-Q中會出現大量積壓,對應的是MQ的Send-Q中出現大量積壓。
從上面的通信模型來看,再次推斷是不是由於客戶端從網絡中讀取字節太慢致使的,由於客戶端爲虛擬機,從netstat 結果來看,疑似是客戶端的問題(備註,其實最後並非客戶端的問題,請別走神)。
2.3.2 網絡轉包
網絡組同事爲了更加嚴謹,還發現了以下的包: 這裏有一個問題很是值得懷疑,就是客戶端與服務端的滑動窗口只有190個字節,一個MQ消息發送返回包大概有250個字節左右,這樣會已響應包須要傳輸兩次才能被接收,一開始覺得這裏是主要緣由,但經過其餘對比,發現不是滑動窗口大於250的客戶端也存在超時,從而判斷這個不是主要緣由,後面網絡同事利用各類工具得出結論,網絡不存在問題,是應用方面的問題。
想一想也是,畢竟是局域網,那接下來咱們根據netstat的結果,將目光放到了客戶端的讀性能上。
2.4 客戶端網絡讀性能瓶頸分析
爲此,我爲了證實讀寫方面的性能,我修改了RocketMQ CLient相關的包,加入了Netty性能採集方面的代碼,其代碼截圖以下: 個人主要思路是判斷客戶端對於一個通道,每一次讀事件觸發,一個通道會進行多少次讀取操做,若是一次讀事件觸發,須要觸發不少次的讀取,說明一個通道中確實積壓了不少數據,網絡讀存在瓶頸。
但使人失望的是客戶端的網絡讀並不存在瓶頸,部分採集數據以下所示: 經過awk命令對其進行分析,發現一次讀事件觸發,大部分通道讀兩次便可將讀緩存區中的數據抽取成功,讀方面並不存在瓶頸,對awk執行的統計分析以下圖所示: 那矛頭又將指向Broker,是否是寫到緩存區中比較慢呢?
2.5 Broker端網絡層面瓶頸
通過上面的分析,Broker服務端寫入pagecache很快,維度將響應結果寫入網絡這個環節並未監控,是否是寫入響應結果並不及時,大量積壓在Netty的寫緩存區,從而致使並未及時寫入到TCP的發送緩衝區,從而形成超時。
筆者原本想也對其代碼進行改造,從Netty層面去監控服務端的寫性能,但考慮到風險較大,暫時沒有去修改代碼,而是再次認真讀取了RocketMQ封裝Netty的代碼,在這次讀取源碼以前,我一直覺得RocketMQ的網絡層基本不須要進行參數優化,由於公司的服務器都是64核心的,而Netty的IO線程默認都是CPU的核數,但在閱讀源碼發現,在RocketMQ中與IO相關的線程參數有以下兩個:
- serverSelectorThreads 默認值爲3。
- serverWorkerThreads 默認值爲8。
serverSelectorThreads,在Netty中,就是WorkGroup,即所謂的IO線程池,每個線程池會持有一個NIO中的Selector對象用來進行事件選擇,全部的通道會輪流注冊在這3個線程中,綁定在一個線程中的全部Channel,會串行進行讀寫操做,即全部通道從TCP讀緩存區,將數據寫到發送緩存區都在這個線程中執行。
咱們的MQ服務器的配置,CPU的核屬都在64C及以上,用3個線程來作這個事情,顯然有點太「小家子氣」,該參數能夠調優。
serverWorkerThreads,在Netty的線程模型中,默認狀況下事件的傳播(編碼、解碼)都在IO線程中,即在上文中提到的Selector對象所在的線程,爲了下降IO線程的壓力,RocketMQ單獨定義一個線程池,用於事件的傳播,即IO線程只負責網絡讀、寫,讀取的數據進行解碼、寫入的數據進行編碼等操做,單獨放在一個獨立的線程池,線程池線程數量由serverWorkerThreads指定。
看到這裏,終於讓我心潮澎湃,感受離真相愈來愈近了。參考Netty將IO線程設置爲CPU核數的兩倍,我第一波參數優化設置爲serverSelectorThreads=16,serverWorkerThreads=32,在生產環境進行一波驗證。
通過1個多月的驗證,在集羣機器數量減小(雙十一以後縮容),只出現過極少數的消息發送超時,基本能夠忽略不計。
舒適提示:關於Netty線程模型的詳解,能夠參考 圖解Netty線程模型
三、總結
本文詳細介紹了筆者排查MQ發送超時的精力,最終定位到的是RocketMQ服務端關於網絡通訊線程模型參數設置不合理。
之因此耗費這麼長時間,其實有值得反思的地方,我被個人「經驗」誤導了,其實之前我對超時的緣由直接歸根於網絡,理由是RocketMQ的寫入PageCache數據很是好,基本都沒有超過100ms的寫入請求,覺得RocketMQ服務端沒有性能瓶頸,並無從整個網絡通訊層考慮。
好了,本文就介紹到這裏了,關注、點贊、留言是對我最大的鼓勵。
掌握一到兩門java主流中間件,是敲開BAT等大廠必備的技能,送給你們一個Java中間件學習路線,助力你們實現職場的蛻變。
最後分享筆者一個硬核的RocketMQ電子書,您將得到千億級消息流轉的運維經驗。
獲取方式:RocketMQ電子書。
舒適提示,本文首發我的網站:https://www.codingw.net