RabbitMQ生產故障問題分析
由某一次真實生產環境rabbitMQ故障引發血案,下面覆盤問題發生原因以及問題解決方法。
1、 問題引發
由某個服務BI-collector-xx隊列出現阻塞,影響很整個rabbitMQ集羣服務不可用,多個應用MQ生產者服務出現假死狀態,系統影響面較廣,業務影響很大。當時爲了應急處理,恢復系統可用,運維相對粗暴的把一堆阻塞隊列信息清空,然後重啓整個集羣。
在覆盤整個故障過程中,我心中有不少疑惑,至少存在以下幾個問題點:
- 爲什麼出現隊列阻塞?
- 某個隊列出現阻塞爲什麼會影響到其他隊列的運行(即多隊列間相互影響)?
- 某個應用MQ隊列出現問題,爲什麼會導致應用不可用呢?
2、 試驗隊列阻塞
某天週末在家裏,找個測試環境,安裝rabbitmq嘗試重現這過程,並做模擬測試。
寫兩個測試應用Demo(假設是兩個項目應用)分別有生產者和消費者,並分別使用隊列testA和testB。
爲了儘可能還原生產的情況,一開始測試使用了同一個vhost,後面分別設置不同vhost。
生產者A,示例代碼如下
消費者A
MQ配置
生產者B,每次生產10萬條消息
消費者B,代碼故意寫錯(模擬出現異常的情況),不是正常的json串導致解釋json時拋出異常
先了解一下Rabbitmq客戶端啓動連接工作過程,通過wireshark抓包分析,如下
先對AMQP做一個簡單的介紹,請求的AMQP協議方法信息,AMQP協議方法包含類名+方法名+參數,這一列主要展示了類名和方法名
Connection.Start
:請求服務端開始建立連接Channel.Open
:
請求服務端建立信道Queue.Declare
:
聲明隊列Basic.Consume
:
開始一個消費者,請求指定隊列的消息
詳細方法可以查看amqp
官網https://www.rabbitmq.com/amqp-0-9-1-reference.html
工作過程分析:
Basic.Publish
:
客戶端發送Basic.Publish
方法請求,將消息發佈到exchange
,
rabbitmq server
會根據路由規則轉發到隊列中;
Basic.Deliver
:
服務端發送Basic.Deliver
方法請求,投遞消息到監聽隊列的客戶端消費者;
Basic.Ack
:
客戶端發送Basic.Ack
方法請求,告知rabbimq server,消息已接收處理。
兩個應用程序啓動後,通過rabbitmq管理控制檯可以觀察一些參數和監控指標
一開始A應用生產和消費都是正常的。
B消費端錯誤代碼異常,狂刷報錯信息
經過大概30分鐘運行,觀察A生產者應用控制檯也有出現異常信息
查看服務端連接狀態出現blocked情況,與生產故障發生情景很類似。
此時客戶端即本機器,CPU和內存上漲明顯,風扇聲音很響,明顯卡頓,再過30分鐘應用基本不可用狀態。
分析原因
上面錯誤代碼展示了消費者B無法ack,由於沒有進行ack導致隊裏阻塞。那麼問題來了,這是爲什麼呢?其實這是RabbitMQ的一種保護機制。防止當消息激增的時候,海量的消息進入consumer而引發consumer宕機。
RabbitMQ提供了一種QOS(服務質量保證)功能,即在非自動確認的消息的前提下,限制信道上的消費者所能保持的最大未確認的數量。可以通過設置prefetchCount實現,自動確認prefetchCount設置無效。
舉例說明:可以理解爲在consumer前面加了一個緩衝容器,容器能容納最大的消息數量就是PrefetchCount。如果容器沒有滿RabbitMQ就會將消息投遞到容器內,如果滿了就不投遞了。當consumer對消息進行ack以後就會將此消息移除,從而放入新的消息。
通過上面的配置發現prefetch初始我只配置了2,並且concurrency配置的只有1,所以當我發送了2條錯誤消息以後,由於解析失敗這2條消息一直沒有被ack。將緩衝區沾滿了,這個時候RabbitMQ認爲這個consumer已經沒有消費能力了就不繼續給它推送消息了,所以就造成了隊列阻塞。
判斷隊列是否有阻塞的風險。
當ack
模式爲manual
,並且線上出現了unacked
消息,這個時候不用慌。由於QOS是限制信道channel
上的消費者所能保持的最大未確認的數量。所以允許出現unacked
的數量可以通過channelCount * prefetchCount *
消費節點數量
得出。
channlCount
就是由concurrency,max-concurrency
決定的。
min = concurrency * prefetch *
消費節點數量
max = max-concurrency * prefetch *
消費節點數量
由此可以得出結論
unacked_msg_count < min
隊列不會阻塞。但需要及時處理unacked
的消息。unacked_msg_count >= min
可能會出現堵塞。unacked_msg_count >= max
隊列一定阻塞。
重點注意
1
、
unacked
的消息在consumer
切斷連接後(如重啓)再連接,會自動回到隊頭。
2、若將ack
模式改成auto
自動,這樣會使QOS不生效。會出現大量消息湧入consumer
從而可能造成consumer
宕機風險。
再回看程序配置,做一些分析和調整
對B消費端問題代碼加個try-catch-finally
,不管中間有何問題,都進行消息簽收ACK。
代碼調整之後,兩個隊列正常運行,客戶端兩個應用也正常運行。
經過一段時間消費,B消費者端已經把堆積的消息消費完了。
3、 第三個問題原因分析
還是查看抓包信息
Basic.Reject
: 客戶端發送Basic.Reject方法請求,表示無法處理消息,拒絕消息,此時的requeue參數爲true,將消息返回原來的隊列;
Basic.Deliver
: 服務端調用Basic.Deliver方法,和第一次Basic.Deliver方法不同的是,此時的redeliver參數爲true,表示重新投遞消息到監聽隊列的消費者,然後這兩步會一直重複下去。
RabbitMQ消息監聽程序異常時,consumer會向rabbitmq server發送Basic.Reject
,表示消息拒絕接受,由於Spring默認requeue-rejected
配置爲true
,消息會重新入隊,然後rabbitmq server重新投遞。就相當於死循環了,所以容易導致消費端資源佔用過高,特別是TCP連接數、線程數、IO飆升,如果個別程序帶事務或數據庫操作等連接資源得不到釋放也會佔滿,導致應用假死狀態(出現問題的時候,查看問題應用出現大量的connection timeout錯誤報錯日誌)。
因此針對性的,有些業務場景(不強調數據強一致性的場景,比如日誌收集)可以設置default-requeue-rejected: false
即可。
factory.setDefaultRequeueRejected(false);
會根據異常類型選擇直接丟棄或加入dead-letter-exchange中。
消費者端正確的使用手動確認示例結構代碼,很重要!
try { // 業務邏輯。 }catch (Exception e){ // 輸出錯誤日誌。 }finally { // 消息簽收。 }
4、 驗證隊列設置最大長度限制
設置queueLengthLimit隊列最大長度限制 x-max-length=5
生產者原本想要生產10條消息
由於受到隊列最大長度限制,實際上只有5條入隊列裏面。
消費者拿出來的消息,僅有5條,從NO.6~NO.10
改變消費者程序,讓生產者一直產生消息,消費者消費速度明顯趕不上生產者的生產速度。
從消費端來看消息是隨機性入隊的,隊列裏面一直最多5條消息,發再多也進不了,消息者和生產者也不會發生什麼異常,只是消息會隨機性丟失(並沒有全部入隊)。
運行情況良好,除了消息沒有全部入隊列 ,沒有出現異常情況
消費比較慢,本機器CPU和內存各項指標正常,沒有異常。
搞一個異常情況出現unack,最大隊列長度限制,是不算unack數量的,如下圖所示
異常之後,此觀察MQ監控管理後臺
生產者不停一直在生產消息,運行30分鐘,觀察生產者應用也是正常的的,就是消息入不了隊列。
5、 檢查實際的業務端代碼
再看我們業務系統消費端代碼,消費端各種不規範寫法都有,以下例舉幾個典型
1、手動簽收有ACK,但是沒有try-catch-finally結構,消費端業務代碼如下:
2、有try-catch-finally結構,但是deliverTag是一個固定值0,一樣的會出問題。
3、自動簽收確認的,大量消息的時候,容易搞死消費端應用。
6、 總結
- 生產環境不建議使用自動ack模式,這樣會使QOS無法生效。
- 在使用手動ack的時候,需要非常注意消息簽收,業務代碼使用try-catch-finally處理結構,防止業務代碼異常時無法簽收。
- 規範約束mq客戶端代碼,正確的使用Rabbitmq配置。
- 不同業務項目設置不同的vhost可以隔離一些影響,提升rabbitmq資源使用。
- 考慮設置dead-letter-exchange,當設置了 requeue=false時,可以放入dead-letter-exchange,可以快速排查定位問題。
- Exchange和隊列的最大長度限制可以是限制消息的數量(參數:x-max-length),或者是消息的總字節數(總字節數表示的是所有的消息體的字節數,忽略消息的屬性和任何頭部信息),又或者兩者都進行了限制,兩者取小值生效,只有處於ready狀態的消息被計數,未被確認的消息不會被計數受到limit的限制。最大隊列設置可以限制生產端,但會造成消息丟失風險,最大消息數量限制,不能完全解決隊列阻塞問題。
- 儘量使用Direct-exchange,Direct 類型的 Exchange 投遞消息是最快的。
- Direct:處理路由鍵,需要將一個隊列綁定到交換機上,要求該消息與一個特定的路由鍵完全匹配。這是一個完整的匹配。如果一個隊列綁定到該交換機上要求路由鍵爲“A”,則只有路由鍵爲“A”的消息才被轉發,不會轉發路由鍵爲"B",只會轉發路由鍵爲“A”;
- Topic:將路由鍵和某模式進行匹配。此時隊列需要綁定要一個模式上。符號“#”匹配一個或多個詞,符號“*”只能匹配一個詞;
- Fanout:不處理路由鍵。只需要簡單的將隊列綁定到交換機上。一個發送到該類型交換機的消息都會被廣播到與該交換機綁定的所有隊列上;
- Headers:不處理路由鍵,而是根據發送的消息內容中的 headers 屬性進行匹配。在綁定 Queue 與 Exchange 時指定一組鍵值對;當消息發送到 RabbitMQ 時會取到該消息的 headers 與 Exchange 綁定時指定的鍵值對進行匹配;如果完全匹配則消息會路由到該隊列,否則不會路由到該隊列。
寫在最後,RabbitMQ集羣做爲整個平臺關鍵部件,它的好處自然不用再說,但是它也不是萬金油,一旦巖機影響很大,後果比較嚴重。怎麼用好它?我們有必要正確深入的認識並使用它,首先得擺好正確的姿勢(寫正確的客戶端代碼、嚴謹的配置),不能隨意,否則後果很嚴重。希望經過此故障經驗教訓能與君共勉,同時也希望我的總結能夠給大家一點幫助和啓發,權當拋磚引玉。