從生產(chǎn)故障解鎖RocketMQ集群部署的最佳實踐
1、故障描述
RocketMQ 集群采取的部署架構(gòu)為2主2從,其部署架構(gòu)如下圖所示:

其部署架構(gòu)中一個非常明顯的特點是一臺物理機上分別部署了 nameserver,broker 兩個進程。
其中一臺機器(192.168.3.100)的內(nèi)存出現(xiàn)故障,導(dǎo)致機器重啟,但Linux操作系統(tǒng)由于重啟需要自檢等因素,整個重啟過程竟然持續(xù)了將近10分鐘,客戶端的發(fā)送超時持續(xù)10分鐘,這顯然是不能接受的?。?!
RocketMQ的高可用設(shè)計何在?接下來我們將詳細介紹其分析過程。
2、故障分析
當?shù)弥慌_機器故障導(dǎo)致消息發(fā)送超時持續(xù)10分鐘,我的第一反應(yīng)是不應(yīng)該呀,因為 RocketMQ 集群是分布式部署架構(gòu),天然支持故障發(fā)現(xiàn)與故障恢復(fù),消息發(fā)送客戶端能自動感知 Broker 異常的的時間絕對不會超過10分鐘,那故障又是怎么發(fā)生的呢?
首先我們來回顧一下RocketMQ的路由注冊與發(fā)現(xiàn)機制。
2.1 RocketMQ路由注冊與剔除機制

其路由注冊、剔除機制說明如下:
集群中所有Broker每隔30s向集群中所有的NameServer發(fā)送心跳包,注冊Topic路由信息。 NameServer在收到Broker端的心跳包時首先會更新路由表,并記錄收到心跳包的時間。 NameServer啟動一個定時任務(wù)每10s掃描Broker存活狀態(tài)表,如果Nameserver 連續(xù)120s未收到Broker的心跳包,將判定該Broker已下線,從路由表中將該Broker移除。 如果Nameserver與Broker端的長連接斷開,NameServer能立即感知Broker下線并從路由表中將該Broker移除。 消息客戶端(消息發(fā)送者、消息消費者)在任意時刻只會和其中一臺NameServer建立連接,并每隔30s向NameServer查詢路由信息,如果查詢到結(jié)果會更新客戶端的本地路由信息;如果查詢路由失敗,則忽略。
從上述路由注冊、剔除機制來看,當一臺Broker服務(wù)器宕機,消息發(fā)送者感知路由信息發(fā)生變化需要的時間是多長呢?
分如下兩種情況分別討論:
NameServer與Broker服務(wù)器TCP連接斷開,此時NameServer能立即感知路由信息變化,將其從路由表中移除,從而消息發(fā)送端應(yīng)該在30s左右就能感知路由發(fā)送變化,在此30s內(nèi)在發(fā)送端會出現(xiàn)消息發(fā)送失敗,但結(jié)合發(fā)送規(guī)避機制,并不會對發(fā)送方帶來重大故障,可接受。 如果NameServer與Broker服務(wù)器的TCP連接未斷開,但Broker已無法提供服務(wù)(例如假死),此時NameServer需要120s才能感知Broker宕機,此時消息發(fā)送端最多需要150s才能感知其路由信息的變化。
但問題來了,為什么一臺Broker由于內(nèi)存故障重啟,10分鐘后業(yè)務(wù)才恢復(fù),即客戶端才真正感知Broker宕機呢?
既然出現(xiàn)了,我們就需要對其進行分析,給出解決方案,避免不會在生產(chǎn)環(huán)境出現(xiàn)同類型的錯誤。
2.2 故障排查經(jīng)過
查詢客戶端的日志(/home/{user}/logs/rocketmqlogs/rocketmq_client.log),從中可以看到從客戶端第一次報消息發(fā)送超時的時間是14:44,其日志輸出如下:

由于192.168.3.100機器內(nèi)存故障,故首先去查看該集群中其他nameserver中的日志,看正常機器中的NameServer感知broker-a故障的時長,其日志如下所示:

從中可以看出192.138.3.101的nameserver基本在2分鐘左右才感知其宕機,即雖然機器在重啟,但可能由于操作系統(tǒng)要做硬件自檢等其他原因,TCP連接并未斷開,故nameserver在120s后才感知其宕機,從路由信息表中將該broker移除,那按照路由剔除機制,客戶端應(yīng)該在150秒的時間內(nèi)感知其變化,那為什么沒感知呢?
繼續(xù)查看客戶端路由信息,查看客戶端感知路由信息發(fā)生變化的時間點,如下圖所示:

從客戶端日志來看,客戶端在14:53:46才感知其變化,這又是為什么呢?
原來客戶端在更新路由信息時報超時異常,其截圖如下所示::

從發(fā)生故障到故障恢復(fù)期間,客戶端一直嘗試從已發(fā)生故障的NameServer去更新路由信息,但一直返回超時,這樣就導(dǎo)致了客戶端一直無法獲取最新的路由信息,故一直無法感知已宕機的Broker。
從日志分析來看,到目前來說就比較明朗了,客戶端之所有沒有在120s之內(nèi)感知其路由信息的變化,是因為客戶端一直嘗試從已宕機的nameserver去更新路由信息,但由于一直無法請求成功,故客戶端的緩存路由信息一直無法得到更新,造成了上面的現(xiàn)象
那問題來了,按照我們對RocketMQ的認識,NameServer宕機,客戶端會自動去從nameserver列表中選擇下一個nameserver,那為什么這里并沒有發(fā)生nameserver切換,而是等到14:53才切換呢?
接下來我們將目光投向NameServer的切換代碼,其代碼片段如下圖所示:

上圖中的幾個關(guān)鍵分析如下:
客戶端從緩存中選用連接用于發(fā)送RPC請求的前提條件是連接的的isActive方法返回true,即底層TCP連接處于激活狀態(tài)。 在客戶端向服務(wù)端發(fā)起RPC請求時,如果出現(xiàn)非超時類異常,會執(zhí)行closeChannel方法,該方法會關(guān)閉連接并從連接緩存表中移除,這個非常關(guān)鍵,因為在切換NameServer時如果緩存中存在連接并連接處于激活狀態(tài),就不會切換nameserver。 如果發(fā)送RPC超時,rocketmq會根據(jù)clientCloseSocketIfTimeout參數(shù)來決定是否關(guān)閉連接,但遺憾的是該參數(shù)默認為false,并且并未提供修改的入口。
那問題分析到這里,已經(jīng)非常明了。
由于機器內(nèi)存故障觸發(fā)重啟并且需要自檢等因素,造成nameserver,broker無法再處理請求但底層TCP連接并未斷開,超時后返回,但客戶端并不會關(guān)閉與故障機器nameserver的TCP連接,不會觸發(fā)切換NameServer,等到機器重新啟動成功后,TCP連接斷開,故障機器重啟完成后感知路由信息變化,故障恢復(fù)。
根本原因:nameserver的假死導(dǎo)致路由信息無法更新。
3、最佳實踐
經(jīng)過上面的故障,個人覺得nameserver不應(yīng)該與broker部署在一起,如果nameserver 與 broker 并不部署在一起,上面的問題能得到有效避免,其部署架構(gòu)如下圖所示:

這樣的部署架構(gòu)如果面對上面的場景,即出現(xiàn)Broker假死的情況,能有效避免嗎?答案是可以的。
如果 192.168.3.100 的 broker 假死,那么 3.110,3.111 的 nameserver 都能在2分鐘內(nèi)感知 broker-a 宕機,客戶端能從nameserver處獲得最新的路由信息,從而在消息發(fā)送時不會繼續(xù)向宕機Broker繼續(xù)發(fā)送消息,故障恢復(fù);
如果nameserver假死,出現(xiàn)超時錯誤,只要broker不宕機,則通過緩存,還是能正常工作的,但如果nanmeserver,broker一起假死,則上述架構(gòu)還是無法規(guī)避上面的問題。
故本次的最佳實踐主要包含如下兩個舉措:
1、nameserver與broker一定要分開部署,進行隔離。
2、nameserver與客戶端的連接,應(yīng)該在超時后,關(guān)閉連接,觸發(fā)nameserver漂移,需要修改源碼。
