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

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

本次直播視頻精彩回顧,戳這裏!
直播回顧: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操做發生丟失。這些可能性都直接致使了消息不可靠。dom

所以,須要額外的措施爲消息提供可靠保障。一種消息可靠性保障方式是,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也是不可靠的。目前提供的生產者確認中的消息重發方案都還存在問題,具體的方案選擇依賴於實際場景和我的取捨。
3、死信隊列


死信隊列使用了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時的一個常見誤區。


原文連接 本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索