1. 程式人生 > >RabbitMQ 的延時佇列和映象佇列原理與實戰

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特性

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

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

 

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

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

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

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

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

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

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模式時,開發者需要注意消費者和生產者的例項數量比例,使訊息生產者產生訊息的速率與消費者消費訊息的速率大致持平。

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

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

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

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

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官方推薦。

另一種Server帶來的資料不可靠問題的解決方案是生產者確認。生產者確認類似於消費端的Ack機制,生產者可能連續傳送多條訊息,Server將這些訊息非同步地通過fsync操作寫入磁碟再非同步地給生產者傳送Ack包,告知生產者訊息的接收成功。由於Ack包非同步傳輸,不影響生產者端訊息的正常傳送。生產者確認模式下,Ack包批量傳送,並且都攜帶有序號,以告知生產者該序號以前的所有訊息都已正常落盤。儘管RabbitMQ推薦使用者使用生產者確認模式,目前的RabbitMQ版本還未實現訊息的重發機制,只實現了Ack包的批量傳送,以通知Client Publisher哪些訊息接收成功。當訊息丟失時,Client Publisher端已publish的訊息在程序掛掉時也可能丟失,而不是重新發送,因此生產者確認的作用也不明顯。當然,生產者確認起到了降低訊息釋出速度的作用,減小了訊息丟失的數量。

生產者確認中的訊息重發可以通過以下幾種方法實現。第一種方式在記憶體中累積還未收到Ack包的訊息,收到Ack包後刪除該訊息,對於一段時間內還停留在記憶體中的訊息,重發該訊息。這種方式將未Ack訊息存入記憶體,一旦訊息生產者宕機,這些訊息也會丟失。另一種方式將未收到Ack包訊息存入磁碟,當收到Ack包後刪除該訊息,然而,磁碟儲存依賴於fsync操作,降低了系統處理訊息的效能。同時,這還會提高程式設計的複雜度,因為這要求釋出訊息時維護檔案佇列,還要求一個非同步執行緒將檔案佇列中的訊息釋出到Server,帶來了多執行緒和鎖問題。還有一種方式將未Ack訊息存入Redis,但當出現網路故障時,Redis也是不可靠的。目前提供的生產者確認中的訊息重發方案都還存在問題,具體的方案選擇依賴於實際場景和個人取捨。

死信佇列

生產者確認中的訊息重發可以通過以下幾種方法實現。第一種方式在記憶體中累積還未收到Ack包的訊息,收到Ack包後刪除該訊息,對於一段時間內還停留在記憶體中的訊息,重發該訊息。這種方式將未Ack訊息存入記憶體,一旦訊息生產者宕機,這些訊息也會丟失。另一種方式將未收到Ack包訊息存入磁碟,當收到Ack包後刪除該訊息,然而,磁碟儲存依賴於fsync操作,降低了系統處理訊息的效能。同時,這還會提高程式設計的複雜度,因為這要求釋出訊息時維護檔案佇列,還要求一個非同步執行緒將檔案佇列中的訊息釋出到Server,帶來了多執行緒和鎖問題。還有一種方式將未Ack訊息存入Redis,但當出現網路故障時,Redis也是不可靠的。目前提供的生產者確認中的訊息重發方案都還存在問題,具體的方案選擇依賴於實際場景和個人取捨。

三、死信佇列

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

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

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

生產環境下使用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官方文件指出,記憶體模式無法提升系統性能,它只提升了產生元資訊資料的速度,即Ram Node指將元資訊存入記憶體,可以提升元資訊的建立速度,而不是訊息資料的效能。這是使用RabbitMQ時的一個常見誤區。

作者:PHP小能手

原文連結

本文為雲棲社群原創內容,未經