跳到主要內容

搜尋酷無極

选择搜索范围,输入关键词后按回车。

語言 / Language

rabbitmq 消費者「無故消失」

問題背景:

9月1日17:12左右,收到實施人員反映,有部分裝置不能正常升級、收不到控制台下發的指令等問題,同事查看control專案(後面簡稱control)那邊的日誌,發現control沒有收到裝置回報的影子資訊,所以沒有下發指令。control專案直接對接裝置,根據裝置回報的資訊對裝置下發一些指令及設定資訊,包括升級、回報日誌等,IoT上線之前control依賴心跳回報來取得裝置的目前資訊,IoT上線之後依賴裝置影子資訊

來取得裝置的目前資訊,control會訂閱裝置的影子資訊,但影子資訊是由影子服務(簡稱IoT)轉送過去的,它不直接對接裝置影子回報,具體流轉細節,請看下面這兩張圖:

應用程式(control)取得裝置狀態

應用程式(control)下發裝置指令

 

得知control收不到影子訊息以後,我立刻去rabbitmq的控制台查看是否有訊息,確認兩件事:1.裝置是否上報了訊息 2.rabbitmq是否正常,下面圖1、圖2是當時擷取的rabbitmq控制台的兩張圖,從圖1可以很清楚地確定裝置有回報訊息,但是有很多訊息是unacked(說明已經投遞給了消費者,只是消費者沒有ack而已,理論上等待一段時間就能恢復正常)的,具體是哪個佇列堆積unacked的訊息請看圖2,“spacebridgeiot-shadow”正是我們用來接收裝置回報的影子資訊的,訊息都堆積到佇列裡了,所以沒有轉發到control也是合理的,觀察了一段時間發現unacked的數量變成了0,但是total的總數卻沒有太大變化,給人的感覺像是unacked的訊息重新回到了訊息佇列裡等待投遞,果然過了幾分鐘以後又發現有大量unacked的訊息,過了幾分鐘以後這部分unacked的訊息重新回到佇列裡,control那邊依然沒有收到訊息,這時查看IoT那邊的日誌發現竟然沒有影子訊息進來,在rabbitmq的主控台查看「spacebridgeiot-shadow”這個佇列下居然沒有消費者了,如圖3所示。這時查看rabbitmq的日誌確實有錯誤訊息,如圖4所示,rabbitmq主動關閉了連線。

圖1:rabbitmq概覽圖

 

 

圖2:rabbitmq佇列統計圖

 

 

圖3:spacebridgeiot-shadow 概覽

 

 

 圖4:rabbitmq 錯誤訊息

 

暫時解決方案:

由於當時已經有大量投訴進來了,所以採用了比較暴力的解決辦法「將堆積的訊息刪除」,刪除以後果然正常了(備註:線上問題必須儘快解決,沒有時間讓我們去分析日誌,然後有條不紊地解決,必須快)。

 

RabbitMQ 技術標誌

透過線下環境重現問題:

1.往10.200.41.166環境的rabbitmq的佇列「mirrorTestQueue」堆積大量訊息(至少上萬筆)

2.停止mirrorTestQueue的消費者,待訊息堆積完成以後重新啟動

3.堆積完成,重新啟動消費者

 

和我們設想的一樣,幾秒內有幾千則訊息推送給了消費者,持續幾分鐘以後rabbitmq主動關閉了和消費者之間的連線,這時從控制台看不到佇列的消費者。由於我們的消費者設定了自動恢復,所以過一陣又會自動連上,

但很快又會被斷線,和我們線上遇到的問題基本一樣,究竟是什麼導致了這個問題呢?說實話當時沒有什麼頭緒,在網路上找了一圈也沒找到什麼特別滿意的答案(當時沒有抓到問題的本質,搜尋的關鍵字太籠統了),後來

我們猜測可能是TCP層面出了什麼問題,所以決定擷取封包試試能不能找到什麼端倪。果然,幸運的事情發生了,話不多說,直接上圖。

 

 

 13:06:25.643428之前 rabbitmq 還一直在給消費者推送訊息,直到13:06:25.643428這個時間點,開始出現消費者 tcp 視窗被塞滿的情況,大概持續了30秒左右,rabbitmq 主動中斷了連線(發了一個 rst 封包),之後消費者重新連線,然後視窗又繼續被塞滿,又持續30秒左右繼續被中斷連線。

 

感覺還挺有規律,每次持續30s,感覺是可設定的一個參數,大概總結一下就是「tcp full window導致了伺服器端主動rst連線,而且還有規律」

這次換了一下搜尋的關鍵字找到了答案,rabbitmq 有一個參數叫tcp_listen_options.send_timeout 是用來控制寫入逾時的一個參數,當寫入逾時以後就會觸發tcp的RST(https://github.com/rabbitmq/rabbitmq-java-client/issues/341),修改一下試試效果如何:

1. 將寫入逾時時間改成10s

tcp_listen_options.send_timeout = 10000

 2.擷取封包看看是否起作用

 

 從視窗滿到關閉連線持續10s左右。

 

現象回顧:

由於rabbitmq的消費端沒有設定prefetch,所以rabbitmq一次向消費端投遞了過多的訊息,從而導致消費端的 tcp 視窗被占滿,進而觸發了rabbitmq 的tcp_listen_options.send_timeout,這個寫入逾時達到一個閾值後會觸發rabbitmq中斷消費者的tcp 連線。

 

終極解決方案:

        之前刪除訊息只是迫不得已的方案,雖然解決了問題但太暴力,我們需要找到一個優雅的方案來應對,既然是推給消費者的訊息太多造成了tcp視窗被塞滿,那我們就應該在接收速率上下點功夫,在連線rabbitmq的時候告訴它別給我傳太多就行。(後面這段話摘自https://blog.csdn.net/james_searcher/article/details/70308565)rabbitmq有一個屬性叫prefetch,prefetch是指單一消費者最多能消費的未確認訊息數量。如何理解呢?mq為每一個 consumer設定一個緩衝區,大小就是prefetch。每次收到一則訊息,MQ會把訊息推送到緩衝區中,然後再推送給用戶端。當收到一則ack訊息時(consumer 發出baseack指令),mq會從緩衝區中空出一個位置,然後加入新的訊息。但是這時候如果緩衝區已滿,MQ將進入阻塞狀態。更具體地描述,假設prefetch值設為10,共有兩個consumer。也就是說每個consumer每次會從queue中預先擷取 10 則訊息到本機快取中等待消費。同時該channel的unacked數變為20。而Rabbit投遞的順序是,先為consumer1投遞滿10則訊息,再往consumer2投遞10則訊息。如果這時有新訊息需要投遞,先判斷channel的unacked數是否等於20,如果是則不會將訊息投遞到consumer中,訊息繼續留在queue中。之後其中一個consumer對一則訊息進行ack,unacked此時等於19,Rabbit就判斷哪個consumer的unacked少於10,就投遞到哪個consumer中。

 

具體到程式碼裡就是

 

 

如何評估這個值呢,rabbitmq官方有篇文章說得很好,就不細說了,我們的系統中目前設定的是20。

https://www.rabbitmq.com/blog/2012/05/11/some-queuing-theory-throughput-latency-and-bandwidth/

 

 

結語:

rabbitmq的使用我們還處於初學者階段,使用之前一定要多看rabbitmq的api,熟悉常用api的用法,在本機多做實驗。


評論區

傳送中

選擇喜歡的樣式

所選樣式會套用至全站。