訊息中介軟體-RabbitMq相關概念及原理介紹【圖文並茂】

後青春期的Keats發表於2022-03-23

訊息中介軟體

訊息中介軟體的作用

  • 解耦:訊息中介軟體在服務之間插入了一個隱含的、基於資料的介面層。兩邊的服務處理過程都要實現這一介面,這允許我們獨立的擴充套件或修改兩邊的處理過程,只要確保他們遵守相同的規範約束即可
  • 冗餘(儲存):訊息中介軟體可以將資料持久化直到完全被處理
  • 擴充套件性:因為訊息中介軟體解耦了應用的處理過程,所以提高訊息入隊和處理的效率都是很容易的,只要另外增加處理過程即可,不需要修改程式碼和調節引數
  • 削峰:在訪問量驟增的情況下,服務仍然需要可用。但以此為標準設計程式又無疑是巨大的浪費。使用訊息中介軟體可以使關鍵元件能夠給支撐突然訪問壓力,不會因為突發的超負荷請求而完全崩潰
  • 可恢復性、順序保證(一定程度)、緩衝、非同步通訊

Kafka

Kafka 和 RabbitMq 的異同

專案中同時用到了 Kafka 和 RabbitMq,因此需要作比較

Kafka 是一個分散式的訊息流處理平臺,擁有著極致的吞吐量,支援訊息重新消費。非常適合應用在大資料領域。因此我們專案中的收數服務應用了 Kafka 來處理每天 10E 級的資料。

RabbitMq 則勝在擁有更靈活的交換器與佇列的匹配規則(基於 topic 交換器 + # * 匹配關鍵字),還有 TTL+死信佇列 實現的延時佇列,及簡單易上手的可靠性保障,因此在吞吐沒有達到每秒幾十萬的而必須用 kafka 時,RabbitMq 是個很好的選擇

RabbitMQ

基礎

相關概念介紹

image-20220316084433569

  • 生產者:投遞訊息的一方,生產者建立訊息,然後釋出到 mq 中。訊息一般分為兩個部分:訊息體和標籤,訊息體也可稱為 payload,實際應用中,訊息體一般是一個帶有業務邏輯結構的資料。訊息的標籤用來表述這條訊息,比如一個交換器名稱和一個路由鍵
  • 消費者:接收訊息的一方,當消費者消費一條訊息時,只是消費訊息的訊息體,訊息路由的過程中,訊息的標籤會被丟棄,存入到佇列中的只有訊息體
  • 佇列:RabbitMq 訊息最終儲存在佇列中,多個消費者可以訂閱同一個佇列,這時佇列中的訊息會被平均分攤。RabbitMq不支援佇列層面的廣播消費,Kafka 可以通過消費者組實現
  • 交換器、路由鍵、繫結:生產者將訊息傳送到交換器,交換器根據 RoutingKey 和 BindingKey 將訊息路由到佇列,如果不能則返回給消費者或丟棄該訊息

交換器型別

  • fanout: 將訊息路由到所有繫結的佇列中
  • direct:將訊息路由到 routingKey 與 bindingKey 完全匹配的佇列中
  • topic:將訊息按照 bindingKey 規則匹配 routingKey ,到指定的佇列
    • routingKey 和 bindingKey 都是以 . 號分割多個單詞的字串
    • BindingKey 可以使用 * 用來匹配一個單詞。# 用來匹配 0 個或多個單詞
  • headers:不依賴路由鍵的匹配,而是使用一個鍵值對匹配,幾乎不怎麼用

佇列的排他性

​ 排他佇列僅對首次宣告他的連線可見,並在連線斷開時自動刪除。該連線下所有的通道(Channel) 都可以使用它

虛擬主機的作用

​ 多租戶場景,對外部而言各個虛擬主機是完全獨立的,A主機的交換器不能繫結B主機的佇列,許可權也是隔離的

Qos

​ 消費者消費速度限制,須配合手動 ack 一起使用,此時當 mq 檢測到某個 channel 未 ack 的訊息達到閾值後,就不會推送訊息到該 cahnnel

延時佇列

可以結合死信佇列 + 佇列過期時間,模擬延時佇列

image-20220320155808121

如何保證訊息不丟失

  • 首先是訊息本身:要確保寫了正確的交換機名、路由鍵名,同時可以設定 mandetory = true 使得訊息沒找到合適的佇列時可以返回給生產者(這樣需要編碼時新增 ReturnListener 的編碼,不方便)。或者新增一個備份交換器,此交換器型別建議設定為 fanout,以保證訊息可以被正確路由到佇列
  • 消費者服務:設定 autoAck = false,程式處理完畢後手動 Ack
  • Mq:開啟交換器、佇列、訊息的持久化。同時為了避免訊息在落盤之前因為 Mq 當機而丟失,採用映象佇列機制(高可用 mq)可以最大程度規避此問題
  • 生產者服務:為了確保訊息成功傳送到 mq ,一般有兩種方法
    • 事物:開啟當前 channel 的事物機制,每傳送一條訊息 commit 一下,如果捕捉到異常則 rollback 同時重新傳送該訊息。該方法實現簡單,但會嚴重降低吞吐量(相比下一種降低 10 倍)
    • 傳送方確認機制:開啟當前 channel 的傳送方確認機制之後,生產者傳送訊息之後,可以立刻或者批量或者非同步等待 mq 響應 ack 或者 nack 訊息,在收到 nack 訊息後做重發操作,其中立即處理和事物機制吞吐差不多,批量會面臨一個訊息 nack 這一批訊息都重新傳送的窘境,非同步確認是最為推薦的機制

擴充套件

Federation 聯邦交換器

假設一種場景:業務A的 clientA 位於北京,需要往位於廣州的 exchangeA 傳送一條訊息,那網路延遲對吞吐量的影響是不容小覷的,如果設定了事務或者開啟了訊息傳送確認,就更慢了

此時可以通過 Federation 外掛解決,在 broker3 中為交換器 exchageA 與 broker1 建立一個單向的 Federation 連線,此時 F 會在 broker1 中建立一個同名交換器 exchangeA,同時建立一個內部交換器 exchageA -> broker3 B,還有一個佇列 f:eA -> b3 B。並與交換器 eA -> b3 B 繫結,F 外掛會在佇列: f:eA -> b3 B 與 broker3 中的 exchangeA 建立 AMQP 連線來實時的消費佇列中的訊息。

這樣部署在北京的生產者可以直接向 exchageA 傳送訊息,可以低延遲的收到回覆。而後訊息通過 F link 轉發到 broker3 的 exchangeA 中,由消費者進行消費

image-20220320220400323

Federation Queue 聯邦佇列

聯邦佇列可以在多個 Broker 節點(或者叢集)之間為單個佇列提供負載均衡的能力。一個聯邦佇列可以連線一個或多個上游佇列(upstream queue),並從上游佇列中獲取訊息以滿足本地消費者的消費需求

如圖所示:佇列 queue1 和 queue2 原本在 broker2 中,由於某種需求將其配置為 fedarated queue 並將 broker1 設定為 upstream queue 。此時 federation 外掛會在 broker1 上建立同名的佇列 queue1 和 queue2,當有消費者 clientA 連線 broker2 並消費 queue1 (queue2) 時,若佇列中有訊息則會直接消費,如果佇列中沒有訊息,那麼它會通過 Federation 從 broker1 中的 queue1(queue2) 中拉取訊息,然後儲存到本地。最後被 clientA 消費

同時 Federate Queue 支援雙向聯邦,一條訊息可以在佇列中被轉發多次,以達到訊息最終被轉發到某一個消費力更強的 broker 中從而被消費

image-20220320221324891

Shovel 鏟子

與 Federation 具備的資料轉發功能類似,Shovel 能夠持續可靠的從一個 Broker 中的 queue 將訊息轉發到當前或另一個 Broker 中的 exchange 中(與 F 不同的是它將訊息由佇列轉發至交換機,而 F 類似於在 B1 建立了一個代理,B1 一開始什麼都不需要有)

其原理是通過消費佇列中的資料同時將資料傳送給交換器來實現資料轉發, Shovel 同時也支援源資料為交換器或者目標資料為佇列。實際上兩者都是通過補足虛擬的佇列或者交換器實現的

案例:訊息堆積的治理

訊息堆積嚴重時,可以選擇清空佇列,或者新增空消費者丟棄部分訊息。但對於重要的資料而言,此舉不可行

另一種方案是增加下游的消費能力,但是這種優化程式碼的方案在緊急時刻缺失“遠水解不了近渴”

那麼合理的優化方案是(一備一):

  1. 建立一個額外的佇列 queue2,通過 shovel 與原佇列 queue1 繫結,當 queue1 中的訊息達到閾值 A 時,通過 shovel 將訊息轉發到 queue2,
  2. 當 queue1 中的訊息減少到閾值 B 時,停止 shovel 轉發
  3. 當 queue1 中的訊息減少到閾值 C 時,將 queue2 的訊息又轉發到 queue1 中
  4. 當 queue1 中的訊息增加到閾值 B 時,停止 shove 轉發。這樣 3 4 迴圈以逐步將多餘訊息消費

如果需要一備多的場景,可以使用映象佇列或 Federation

原理

儲存機制

不管是持久化還是非持久化的訊息都可以被寫入到硬碟。持久化的訊息在到達佇列時就被寫入到磁碟,並且如果可以,持久化的訊息也在記憶體中儲存一份備份,當記憶體吃緊的時候從記憶體中清除。非持久化的訊息一般只儲存在記憶體中,當記憶體吃緊的時候會被換入磁碟中,以節省記憶體空間。這兩種訊息的落盤處理都在 RabbitMq 的“持久層”完成

“持久層”實際上是一個邏輯概念,實際包含兩個部分:佇列索引 rabbit_queue_index 和訊息儲存 rabbit_msg_store

rabbit_queue_index 負責維護佇列中落盤的訊息,包括訊息的儲存地點、是否已交付給消費者、是否已 Ack,每個佇列與之對應的 rabbit_queue_index

rabbit_msg_store 以鍵值對的形式儲存訊息,它被所有佇列共享,在每個節點有且只有一個

訊息(包括訊息體、屬性和 headers)可以儲存在兩者中的任意一個。一般通過 queue_index_embed_msg_below 配置一個大小閾值,較小的訊息儲存在 rabbit_queue_index 中,較大的訊息儲存在 rabbit_msg_store 中

佇列的結構

通常佇列由 rabbit_amqqueue_process 和 backing_queue 這兩部分組成,rabbit_amqqueue_process 負責協議的相關訊息處理,backing_queue 是訊息儲存的具體形式和引擎,訊息入佇列之後,不是固定不變的,它會隨著系統的負載不斷的流動,有以下四種狀態

  • alpha:訊息內容和訊息索引都儲存在記憶體中
  • beta:訊息索引儲存在記憶體中,訊息內容儲存在磁碟中
  • gamma:訊息內容儲存在磁碟中,訊息索引儲存在記憶體和磁碟中
  • delta:訊息內容和訊息索引都儲存在磁碟中

對於持久化的訊息,訊息內容和訊息索引必須先儲存在磁碟上,才會處於上述狀態的一種,而 gamma 狀態的訊息是隻有持久化的訊息才會有的狀態。對於 durable 為 true 的訊息,在開啟 publish confirm 機制後,只有到了 gamma 狀態才會確認訊息已被接收

如圖所示:Q1 和 Q4 僅儲存 alpha 狀態的訊息,Q2 和 Q3 儲存 beta 和 gamma 狀態的訊息,Detla 儲存 detla 狀態的訊息,當消費者消費訊息時,會先從 Q4 從獲取,如果成功則返回,如果 Q4 為空則按照一定的規則從上面的佇列中轉移訊息到 Q4 後獲取

通常負載正常時,對於不需要保證訊息可靠不丟失的情況,極有可能訊息只處於 alpha 狀態。對於需要持久化的訊息,只有當訊息處於 gamma 狀態時才會確認訊息已接收。

image-20220321081054876

惰性佇列

惰性佇列會盡可能的將訊息儲存在硬碟之中,而在消費者消費到相應的訊息才會載入到記憶體中。惰性佇列會將接收到的訊息直接儲存到檔案系統中,而不管訊息是持久化的還是非持久化的,這樣可以減少記憶體的損耗

流控

image-20220321200817440

RabbitMq 的流控鏈如上圖所示

  • rabbit_reader:connection 的處理程式,負責接收、解析 AMQP 協議資料包等
  • rabbit_channel:Channel 的處理程式,負責處理 AMQP 協議中的各種方法,進行路由解析等
  • rabbit_amqqueue_process:佇列的處理程式,負責實現佇列的所有邏輯
  • rabbit_msg_store:負責實現佇列的持久化

當 connection 處於 flow 狀態,而 connection 沒有一個 channel 處於 flow 狀態,說明 channel 出現了效能瓶頸,一般是因為處理大量較小的非持久化訊息時出現

當 connection 處於 flow 狀態,並且若干個 channel 處於 flow 狀態,但是沒有任何一個對應的佇列處於 flow 狀態。說明一個或多個佇列出現了效能瓶頸,這可能是將訊息存入佇列時 CPU 佔用過高,或者將訊息持久化到磁碟時 I/O 過高,這種情況一般會在處理大量較小的持久化訊息時出現

當 connection、channel、若干佇列都是 flow 狀態時,意味著在訊息持久化時出現了效能瓶頸,這種情況一般在傳送大量的較大持久化訊息時最容易出現

打破佇列的瓶頸

向一個佇列中推送訊息時,往往會在 rabbit_amqqueue_process(即佇列程式中)產生效能瓶頸。那如何破局,提高 rabbit 的效能呢

image-20220321202119070

如圖所示,因為 rabbit_amqqueue_process 是佇列獨享的,而在程式碼層面實現多個佇列會增加業務的複雜度,因此可以通過封裝拆分佇列的邏輯來解決

映象佇列

如果 RabbitMq 只有一個 Broker 節點,那麼該節點的失效將會導致整體服務的暫時不可用,並且有可能導致訊息的丟失。可以將訊息設定為持久化,並且將訊息所屬的佇列 durable 屬性設定為 true,但這仍無法避免快取導致的問題,因為訊息在傳送之後到存檔之前有一個短暫的時間窗。通過 publish confirm 機制可以保證訊息落盤後確認(前文有提到,broker 會在訊息進入 gamma 階段也即訊息體存檔、訊息索引磁碟和記憶體都有的時候,通知生產者訊息傳送成功),儘管如此,我們仍不希望 Broker 單點導致的服務不可用問題

映象佇列機制可以將佇列映象到叢集中的其他 broker 上,如果叢集中的一個 broker 失效了,佇列能自動的切換到映象中的另外一個節點保證服務的可用性,每一個映象佇列都包含一個主節點 master,和若干個從節點 slave,相應的結構圖如下

image-20220322212535081

slave 會準確按照 master 的執行命令的順序進行動作,如果 master 當機,"資歷最老"(加入時間最長)的 slave 會提升成 master,傳送到映象佇列的訊息會同時傳送給 master 和 slave(圖中實線),除傳送訊息外的所有動作只會和 master 打交道,然後由 master 同步給 slave(圖中虛線)。同步採用的是一種稱為組播 GM(Guaranteed Multicast) 的方式,GM模組的實現是一種可靠的組播通訊協議,該協議能保證組播訊息的原子性,即保證組中活著的節點要麼都收到訊息要麼都收不到,它的實現大致如圖上所示,所有節點形成一個迴圈連結串列,master 發出的訊息最終會再次收到,以此確認組中所有節點都收到。

可能有人會覺得,消費者都是從 master 讀取訊息的,broker 之間是不是沒有得到有效的負載均衡?其實不然,負載均衡是對整個 broker 而言,對整個機器而言的,而消費者消費的是佇列,只要確保佇列的 master 節點均勻的散落在不同的 broker 上,即可確保很大程度的負載均衡

image-20220322213726488

RabbitMq 的映象佇列機制同時支援事物和 publisher confirm 兩種機制,在事物機制中,只有當前事物在所有節點中都執行之後,才會返回 OK,同樣的在 publisher confirm 機制,只有當所有映象都接收該訊息並處於 gamma 狀態時,才會通知生產者

相關文章