RabbitMQ 的延時隊列和鏡像隊列原理與實戰

摘要:在阿里雲棲開發者沙龍PHP技術專場上,掌閱資深後端工程師、掘金小測《Redis深度歷險》作者錢文品為大家介紹了RabbitMQ的延時隊列和鏡像隊列的原理與實踐,重點比較了RabbitMQ提供的消息可靠與不可靠模式,同時介紹了生產環境下如何使用RabbitMQ實現集群間消息傳輸。

本次直播視頻精彩回顧,戳這裡!

直播回顧:https://yq.aliyun.com/live/965

PPT分享:https://yq.aliyun.com/download/3529

本文根據演講視頻以及PPT整理而成。

本文將主要圍繞以下四個方面進行分享:

  1. RabbitMQ特性
  2. RabbitMQ中的消息不可靠問題及其解決方案
  3. 死信隊列
  4. 生產環境下使用RabbitMQ應注意的事項

RabbitMQ特性

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

對於左邊的Client Publisher而言,RabbitMQ Server是消息的接收者,也就是消費者;對於右邊的Client Consumer而言,RabbitMQ Server是消息的發送者,也就是生產者。RabbitMQ Server將消息從Client Publisher傳送給Client Consumer,扮演著消息中間商的角色。

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

RabbitMQ Server負責將Client Publisher傳遞來的消息持久化,延後地將消息傳遞給Client Consumer.這樣,即使消費者掛掉,RabbitMQ Server也可以存儲消息,當消費者重新工作時再將存儲的消息傳遞過去,從而保證消息不丟失。RabbitMQ Server提供了堆積消息的能力。

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

另外,RabbitMQ Server還具有複製和廣播消息的能力。具體來說,RabbitMQ Server可以將Client Publisher發佈的消息分發給多個消費者,比如它能夠將特定的消息按照特定的隊列分發給特定的消費者。“特定”指不同消息具有不同的routing key屬性,由上圖實例,不同的消息生產者生產了具有不同routing key的消息,通過exchange路由器將不同的routing key消息投遞到不同隊列,從而分發給不同消費者。

RabbitMQ中的消息不可靠問題及其解決方案

消費端消息不可靠問題及其解決方案

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

實際上,RabbitMQ Server將消息投遞給消費者,具有消息不可靠的特點。具體來說,RabbitMQ Server將消息投遞給消費者時會調用套接字的write操作,而write操作的過程是不可靠性的。在write操作的過程中,Server需要將消息發送到套接字的緩存中,通過網卡轉發到鏈路上,最終到達消費者所在的機器內核的套接字緩存中,由消費者使用套接字的read操作將消息讀出來。

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

即使套接字的write操作成功也無法保證消息可靠,潛在的網絡故障可能使消費者接收不到消息。機器宕機也可能使消息不可靠,即使消息字節流已經到達消費者所在機器,消費者所在機器的宕機也可能使消息無法被即時讀取並處理。另外,即使消費者即時讀取消息,內存消息隊列中的所有消息也可能因為kill-9操作發生丟失。這些可能性都直接導致了消息不可靠。

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

因此,需要額外的措施為消息提供可靠保障。一種消息可靠性保障方式是,Server投遞消息後並不立即將消息從Server刪除,而是等到消費者接收、處理消息並返回Ack包給Server後,Server才刪除該消息。如果消費者沒有發送Ack包,那麼Server將重新投遞該消息。這個過程確保消息被消費者處理,保證了消息可靠。另外,假如消費者已處理消息併發送Ack包給Server,但由於網絡故障等問題導致Ack包丟失時,那麼Server同樣會重新投遞該消息,導致消息被重複處理。消息的重複處理通常由業務層面的技術手段來避免,比如在數據庫層面添加主鍵約束等。另一種重複消息處理的避免方式是客戶端對每條消息維護ID, 將被處理消息的ID記錄在列表中,同時檢查新到消息是否在該列表中。

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

RabbitMQ中的Auto Ack和Manual Ack對應著消息不可靠模式和消息可靠模式. Auto Ack即no ack,指消息投後即刪除,對應消息不可靠傳輸。Manual Ack即手動Ack,消費者處理完消息後使用Ack包通知Server刪除消息,對應消息可靠傳輸。

Auto Ack是RabbitMQ中最常用的模式,性能較好,但具有以下問題。當消息通過套接字write操作投遞後,RabbitMQ Server立即刪除該消息,該模式在遇到網絡故障時容易發生消息丟失。另外,假如消費者處理消息的速率過低,可能導致消息在消費者recv buffer中大量堆積,從而導致Server端send buffer也堆積大量消息, Server端無法繼續調用套接字write操作。這樣,一段時間之後,Server可能強制關閉消息傳輸鏈接,導致消息不可傳輸。

儘管Auto Ack存在一定風險,目前許多公司仍在應用Auto Ack模式。使用Auto Ack模式時,開發者需要注意消費者和生產者的實例數量比例,使消息生產者產生消息的速率與消費者消費消息的速率大致持平。

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

Manual Ack是RabbitMQ 中更加智能的一種模式。Manual Ack在工作時會考慮消息消費者的消息接收能力,根據消費者的消息接受能力和當前接收到的Ack包自動調節分發消息的速率,保證消息分發可靠、不阻塞。具體來說,客戶端通過PrefetchCount告知Server自身堆積消息的能力。

生產端消息不可靠問題及其解決方案

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

消息生產端同樣存在消息的可靠性問題。從Client Publisher將消息傳遞給Server和從Server將消息傳遞給Client Consumer的過程是完全對等的,Server和Client Consumer間傳遞消息的可靠性問題在Client Publisher和Server間同樣存在。

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

Client Publisher首先將消息寫到套接字,再通過網絡傳遞給Server的套接字buffer,最終由Server讀取該消息。這一過程的潛在網絡問題也可能使Server端接收不到消息。

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

另外,Server端本身也可能導致消息不可靠。Server端需要持久化消息,但出於性能開銷的考慮,Server端並不在每次持久化消息時都刷盤。具體來說,Server端會對文件執行write操作,將髒數據寫入操作系統的緩存中,而不是立即將數據寫入磁盤。一般情況下,Server可能每幾百毫秒執行一次fsync操作,通過fsync操作將文件的髒數據寫入磁盤。由於Server具有宕機風險,那麼每次Server宕機時,還未被fsync操作處理的數據就可能丟失,此過程類似於Redis AOF。

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

RabbitMQ通過生產者事務和生產者確認兩個方法解決Server產生的數據不可靠問題。

生產者事務的基本原理是採用select和commit指令包裹publish,在消息生產者publish數據之前執行select操作,相當於begin transaction事務開始,在執行若干個publish操作後,再執行commit操作,相當於提交事務。根據tcp包的有序性,commit包成功接收意味著commit包之前的包也成功接收。

因此,收到從Client Publisher傳遞過來的commit包意味著該commit包之前的所有publish包都已成功接收,即所有消息都成功接收。然而,commit包只有等到Server端的fsync操作執行完畢時才返回,因此生產者事務的效率較低,通常只在有批量publish操作時才使用生產者事務模式。也就是說,客戶端將消息累計起來批量發送,以降低fsync操作帶來的性能損失。此外,在進程中累計消息也存在風險,累計的消息可能由於進程掛掉而丟失。總的來說,生產者事務由於性能缺點不被RabbitMQ官方推薦。

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

另一種Server帶來的數據不可靠問題的解決方案是生產者確認。生產者確認類似於消費端的Ack機制,生產者可能連續發送多條消息,Server將這些消息異步地通過fsync操作寫入磁盤再異步地給生產者發送Ack包,告知生產者消息的接收成功。由於Ack包異步傳輸,不影響生產者端消息的正常發送。生產者確認模式下,Ack包批量發送,並且都攜帶有序號,以告知生產者該序號以前的所有消息都已正常落盤。

儘管RabbitMQ推薦用戶使用生產者確認模式,目前的RabbitMQ版本還未實現消息的重發機制,只實現了Ack包的批量發送,以通知Client Publisher哪些消息接收成功。當消息丟失時,Client Publisher端已publish的消息在進程掛掉時也可能丟失,而不是重新發送,因此生產者確認的作用也不明顯。當然,生產者確認起到了降低消息發佈速度的作用,減小了消息丟失的數量。

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

生產者確認中的消息重發可以通過以下幾種方法實現。第一種方式在內存中累積還未收到Ack包的消息,收到Ack包後刪除該消息,對於一段時間內還停留在內存中的消息,重發該消息。這種方式將未Ack消息存入內存,一旦消息生產者宕機,這些消息也會丟失。另一種方式將未收到Ack包消息存入磁盤,當收到Ack包後刪除該消息,然而,磁盤存儲依賴於fsync操作,降低了系統處理消息的性能。

同時,這還會提高編程的複雜度,因為這要求發佈消息時維護文件隊列,還要求一個異步線程將文件隊列中的消息發佈到Server,帶來了多線程和鎖問題。還有一種方式將未Ack消息存入Redis,但當出現網絡故障時,Redis也是不可靠的。目前提供的生產者確認中的消息重發方案都還存在問題,具體的方案選擇依賴於實際場景和個人取捨。

死信隊列

生產者確認中的消息重發可以通過以下幾種方法實現。第一種方式在內存中累積還未收到Ack包的消息,收到Ack包後刪除該消息,對於一段時間內還停留在內存中的消息,重發該消息。這種方式將未Ack消息存入內存,一旦消息生產者宕機,這些消息也會丟失。另一種方式將未收到Ack包消息存入磁盤,當收到Ack包後刪除該消息,然而,磁盤存儲依賴於fsync操作,降低了系統處理消息的性能。

同時,這還會提高編程的複雜度,因為這要求發佈消息時維護文件隊列,還要求一個異步線程將文件隊列中的消息發佈到Server,帶來了多線程和鎖問題。還有一種方式將未Ack消息存入Redis,但當出現網絡故障時,Redis也是不可靠的。目前提供的生產者確認中的消息重發方案都還存在問題,具體的方案選擇依賴於實際場景和個人取捨。

三、死信隊列

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

死信隊列使用了RabbitMQ中的一種特殊隊列屬性,即x-message-ttl屬性,表示隊列中消息的構建時間。假如用戶在聲明隊列時定義隊列的x-message-ttl屬性,此後所有進入該隊列的消息都將持有構建時間,到達構建時間的消息將被刪除。如果還為隊列配置了回收站屬性,那麼即使構建時間到達,RabbitMQ也不會立即刪除這些消息,而是將這些過期消息丟入回收站,即死信隊列。

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

死信隊列的工作方式如上圖。Client Publisher將消息投遞給路由器,也就是exchange,再由exchange將消息投遞給隊列,由隊列生成該消息的構建時間,到達構建時間的消息將過期,同時進入死信隊列。過期消息進入死信隊列的方式和進入普通隊列的方式基本一致,即先投遞給exchange路由器,再由exchange投遞消息。消費者消費死信隊列,得到的消息是延後的消息,延遲的時間長度即構建時間。目前,死信隊列存在的問題是,一個隊列只能設置一個構建時間,消息的過期時間不夠靈活,不能滿足一些特殊場景的需求,比如動態的重試時間。

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

死信隊列的另一個使用場景是Retry Later,即在一段時間後才重新處理此前處理失敗的消息,這時可能用到雙重死信。具體來說,死信隊列不僅可以接收過期消息,還可以接收被reject的消息,即消費端拒絕處理或處理過程發生異常的消息,Reject操作具有requeue參數,當requeue設為true時被reject消息會重新進入消息隊列並被重新投遞,當requeue設為false時被reject消息將進入死信隊列。假如死信隊列持有構建時間,那麼到達構建消息的消息將重新投遞給原有隊列,實現Retry Later。雙重死信在使用過程中需注意消息處理的死循環問題,因為消息可能無限循環地進入死信隊列。

生產環境下使用RabbitMQ應注意的事項

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

生產環境下,RabbitMQ通過使用集群模式。集群模式下,只有元信息分佈在所有節點中。元信息指隊列信息,路由器信息等,隊列中的信息只存儲在一個節點中,因此,單個節點宕機會導致所有節點都不可用。另外,RabbitMQ的所有節點間存在轉發機制,即允許節點轉發其他目標節點的消息處理請求,這樣客戶端只需連接到任意一個節點就可以實現其消息轉發需求。

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

隊列的高可用依賴於RabbitMQ的鏡像隊列,即在其他節點上備份某節點的消息內容。這樣,當消息所在主節點宕機時,其他鏡像節點可以替代主節點完成消息傳遞任務。

RabbitMQ 的延時隊列和鏡像隊列原理與實戰

通常情況下,鏡像節點是默默無聞的,客戶端無需感知鏡像節點的存在。只有當主節點宕機時,鏡像節點才發揮作用。鏡像隊列的配置如下:

  • Ha-mode具有三個選項,all指將所有隊列的信息存入所有節點,這種模式最安全,但也最浪費存儲空間;exactly指由用戶精確指定每個隊列的複製數,當ha-mode設置為exactly,ha-params設置為2時表示“一主一從”,這種模式是官方推薦的;nodes指由用戶指定副本所在的節點,這種模式極少被使用。
  • x-queue-master-locator用於設置存儲隊列主節點的RabbitMQ節點。min-master指將隊列主節點設置在隊列數量最少的RabbitMQ節點,client-local指將隊列主節點設置在當前客戶端所在的RabbitMQ節點,random即隨機選擇節點。
  • Ha-sync-mode用於鏡像節點代替宕機主節點並創建新節點以彌補缺失節點時,設置新節點上數據的同步策略。automatic指自動地將新主節點上數據全部同步給新節點,manual指不同步新主節點上的老數據,只同步新產生的數據。由於節點間數據同步需要耗費時間,長時間的數據同步可能會影響服務的穩定性,但通常情況下RabbitMQ的節點堆積的數據量並不大,因此RabbitMQ官方推薦使用Automatic進行數據同步。
  • Ha-sync-batch-size指節點間批量同步的數據量。
  • Ha-promote-on-shutdown表示主動停止主節點的服務時,其他節點如何替代主節點。Always指其他節點總是能順利地替代主節點,when-synced要求與原主節點數據完全一致的節點才能替代主節點。
  • Ha-promote-on-failure表示異常情況下其他節點如何替代主節點,always和when-synced的含義與Ha-promote-on-shutdown中一致。
RabbitMQ 的延時隊列和鏡像隊列原理與實戰

許多公司為RabbitMQ集群設置了內存模式,認為內存模式無需落盤,能夠提升系統性能。但實際上,RabbitMQ官方文檔指出,內存模式無法提升系統性能,它只提升了產生元信息數據的速度,即Ram Node指將元信息存入內存,可以提升元信息的創建速度,而不是消息數據的性能。這是使用RabbitMQ時的一個常見誤區。


分享到:


相關文章: