RabbitMQ——短連接惹的禍

【前言】

最近在生產環境出現了一個奇怪問題,並且該問題多次出現,問題排查過程中對一些線索大膽猜測其問題的原因,最終找了了問題的根因。這裏進行總結,方便後續回顧。

【問題現象】

環境背景與具體的現象:隊列與路由到隊列中的消息均爲持久化;隊列設置了最大長度爲100W;同時隊列設置爲lazy模式;隊列實際只堆積了30-50W的消息;隊列裏有一個消費者。

但是,消費者幾乎無法從隊列消費到消息,並且內存在不斷的增加,最嚴重時,內存超過了設置的高水位,最終導致整體不可用

【問題分析】

問題出現時,第一反應懷疑消費者是不是有問題,因爲我們的消費者一般都開啓了ACK機制,同時設置了一定大小的prefetch_count。如果消費者沒有及時進行ACK,導致unack數目等於prefetch_count的值,那麼這個時候服務端確實是不會繼續給消費者推送消息的。

然而,實際情況是隊列的unack持續爲0,這就意味着是rabbitmq沒有給消費者推送消息。

出於不死心的心態,又把模擬消費的客戶端放上去,想嘗試到隊列消費消息,結果都無法成功進行訂閱

既然客戶端無法訂閱並消費消息,索性從WEB界面上直接GET消息,結果依舊是沒有任何響應

這時,自己也產生了疑惑,明明隊列裏有消息,爲什麼不給消費者推送,GET請求也沒有任何響應

帶着疑惑打開了rabbitmq_top插件,發現有問題的這個隊列的gen_server2 buffer中竟然300W+的消息,並且還在不斷增加。馬上又通過process_info查看了該隊列進程的信息,發現隊列進程字典中有100W+的credit_to記錄

隊列進程中100W+的credit_to記錄就意味着當前或曾經有100W+的生產者(不懂隊列進程字典中credit_to記錄與生產者的關係的可以看下《RabbitMQ——流控》),然而實際發現很多credit_to記錄對應的進程並不存在。於是大膽猜測生產者採用了"短連接"的方式,也就是每次發送消息時都新創建一條TCP連接,或者同一TCP連接上新打開一個通道,發送完消息後,關閉了連接或通道,並不斷進行重複。

爲了驗證猜測,反推找到隊列對應生產者的連接,在WEB界面上看到了該生產者連接的通道信息在不斷變化,一會有1000多個通道,一會一個也沒有了。同樣,tcpdump抓包也進一步確認了生產者對應的連接上在不斷重複的打開通道,發送消息,關閉通道。

至此,斷定就是生產者採用了短連接的方式進行消息的發送導致了本次問題。與對應的開發人員溝通,改成了長連接的方式後,問題得以解決。

【原理分析】

問題雖然是解決了,但仍舊有疑惑:例如隊列進程中大量的credit_to記錄與buffer的堆積有什麼關聯?爲什麼buffer的堆積又會導致無法消費(GET)到消息。

帶着疑問在測試環境中進行問題的復現,同時查看相關源碼,發現了其中的問題。

首先來說下gen_server2 buffer是什麼。

erlang中的每個進程都各自有一個郵箱,進程與進程通信的方式是將消息投遞到對方的郵箱中,進程對郵箱中的消息採用模式匹配的方式進行處理(模式匹配涉及erlang的語法知識,這裏不展開說明,讀者可先簡單理解爲從郵箱中逐一取出消息並進行處理)

而rabbitmq自己實現了一套處理邏輯,那就是不斷從郵箱中接收消息,然後放到一個buffer中,然後再不斷從這個buffer中取出消息進行相應的處理。

這裏說的進程郵箱也就是rabbitmq_top插件開啓後,web界面上顯示的Erlang mailbox;而這裏說的buffer,就是web界面上顯示的gen_server2 buffer。

注意:

1、這個buffer的實現實際上是一個優先級隊列,每個投遞給隊列進程的消息,從郵箱中取出進行處理時會先回調上層模塊確認消息的優先級,然後再根據優先級插入到buffer中。高優先級的消息會被放到buffer的頭部,低優先級的消息會被放到buffer的尾部,隊列進程每次都是從buffer的頭部取消息進行處理,這就意味着,高優先級的消息會優先被處理

2、位於這個buffer中的消息都是存放在內存中的,這樣就能解釋爲什麼隊列和消息都是持久化的,隊列也設置了lazy屬性,並且隊列實際上並沒有堆積很多的情況下,buffer中消息的增加會導致整體內存的增加。

既然進入隊列的消息都是有優先級的,那麼哪些消息是高優先級的,哪些是相對較低的。這裏貼出對應的源碼片段

這裏也有幾點要注意

1、"_"表示匹配除上面指定外的其他任何消息,也就包括正常生產的消息,GET請求的消息。

2、消費者訂閱請求的消息有兩個優先級,在隊列生產消費速度都很低的情況下爲0,反之爲2。

隊列進程收到生產者發送的消息後,會對生產者的通道進行monitor,如果此時生產者的通道關閉,隊列進程會收到通道DOWN的消息(該消息優先級爲8)。因此,就存在這麼一種情況,生產者使用"短連接"的方式持續發送大量消息,隊列收到這些消息並且在處理的過程中生產者通道關閉了,那麼通道DOWN的消息會因爲優先級較高而被插入到了buffer的頭部。這個時候,隊列一段時間內一直都在處理通道DOWN的消息(需要從進程字典中找到對應的credit_to記錄並刪除,除此之外還有其他的處理動作),對隊列而言,此時的進出速率爲0,因此消費者訂閱的請求消息,GET請求的消息,生產者發送的消息投遞到隊列進程後,都會被放到buffer的尾部,必須等前面的消息都處理完後才能得到響應。

這樣就很好的解釋了爲什麼隊列進程字典中有大量的credit_to記錄後,一段時間內會導致消費者卻無法進行訂閱,也無法按從隊列GET到消息了。

【總結】

排查問題需要大膽猜測,然後實測並結合源碼小心驗證。

【彩蛋】

結合前面提到的消息優先級,設想這麼一種場景:

假如生產者發送消息後,消息還在隊列的buffer中排隊等待處理,此時生產者關閉了通道或TCP連接,這種情況下,生產者發送的消息還能被隊列正確處理嗎?如果能處理的話,是不是意味着隊列進程字典中還會記錄對應的credit_to信息?如果記錄了,那麼這個記錄還有機會被刪除嗎?