RabbitMQ的前世今生

Java大蝸牛發表於2018-12-06

關於RabbitMQ

出身: 誕生於金融行業的訊息佇列

語言: Erlang

協議: AMQP(Advanced Message Queuing Protocol 高階訊息佇列協議)

關鍵詞: 記憶體佇列,高可用

一條訊息

佇列結構

RabbitMQ-Arch

  • Producer/Consumer:生產者消費者

  • Exchange:交換器,可以理解為佇列的路由邏輯,交換器主要有三種,圖中是Direct交換器

  • Queue:佇列

  • Binding:繫結關係,實際是交換器上對映佇列的規則

傳送和消費一條訊息

在上圖的模式下,交換器的型別為Direct,虛擬碼表示訊息的生產和消費

訊息生產

#訊息傳送方法#messageBody 訊息體#exchangeName 交換器名稱#routingKey 路由鍵publishMsg(messageBody,exchangeName,routingKey){
	......
}#訊息傳送publishMsg("This is a warning log","exchange","log.warning");

Produce

RoutingKey=log.warning,和佇列A與交換器的繫結一致,所以訊息被路由到了佇列A上

訊息消費

對於訊息消費而言,消費者直接指定要消費的佇列即可,比如指定消費佇列A的資料。

需要注意的是,在消費者消費完成資料後,返回給RabbitMq ACK訊息,RabbitMq會刪掉佇列中的該條資訊。

Consume

多種訊息路由模式

在Exchange這個模組上,RabbitMq主要支援了Direct,Fanout,Topic三種路由模式,RabbitMq在路由模式上下功夫,也說明了他在設計上想要滿足多樣化的需求。

Routing

Direct和Fanout模式比較好理解,類似於單播和廣播模式,Topic模式比較有意思,它支援自定義匹配規則,按照規則把所有滿足條件的訊息路由到指定佇列,能夠幫助開發者靈活應對各類需求。

訊息的儲存

RabbitMQ的訊息預設是在記憶體裡的,實際上不光是訊息,Exchange路由等資訊實際都在記憶體中。記憶體的優點是高效能,問題在於故障後無法恢復。所以RabbitMQ也支援持久化的儲存,也就是寫磁碟。

要在RabbitMQ中持久化訊息,要同時滿足三個條件:

  1. 訊息投體時使用持久化投遞模式

  2. 目標交換器是配置為持久化的

  3. 目標佇列是配置為持久化的

RabbitMQ持久化訊息的方式是常見的寫日誌方式:

  1. 當一條持久化訊息傳送到持久化的Exchange上時,RabbitMQ會在訊息提交到日誌檔案後,才傳送響應

  2. 一旦這條訊息被消費後,RabbitMQ會將會把日誌中該條訊息標記為等待垃圾收集,之後會從日誌中清除

  3. 如果出現故障,自動重建Exchange,Bindings和Queue,同時透過重播持久化日誌來恢復訊息。

訊息持久化的優缺點很明顯,擁有故障恢復能力的同時,也帶來了效能的急劇下降。同時,由於RabbitMQ預設情況下是沒有冗餘的,假設一個持久化節點崩潰,一致到該節點恢復前,訊息和佇列都無法恢復。

訊息投遞模式

1.發後即忘

RabbitMQ預設釋出訊息是不會返回任何結果給生產者的,所以存在傳送過程中丟失資料的風險

2.AMQP事務

AMQP事務保證RabbitMQ不僅收到了訊息,併成功將訊息路由到了所有匹配的訂閱佇列,AMQP事務將使得生產者和RabbitMQ產生同步。

雖然事務使得生產者可以確定訊息已經到達RabbitMQ中的對應佇列,但是卻會降低2~10倍的訊息吞吐量。

3.傳送方確認

開啟傳送方確認模式後,訊息會有一個唯一的ID,一旦訊息被投遞給所有匹配的佇列後,會回撥給傳送方應用程式(包含訊息的唯一ID),使得生產者知道訊息已經安全到達佇列了。

如果訊息和佇列是配置成了持久化,這個確認訊息只會在佇列將訊息寫入磁碟後才會返回。如果RabbitMQ內部發生了錯誤導致這條訊息丟失,那麼RabbitMQ會傳送一條nack訊息,當然我理解這個是不能保證的。

這種模式由於不存在事務回滾,同時整體仍然是一個非同步過程,所以更加輕量級,對伺服器效能的影響很小。

RabbitMQ RPC

一般的非同步服務間,可能會用兩組佇列實現兩個服務模組之前的非同步通訊,有趣的是RabbitMQ就內建了這個功能。

RabbitMQ支援訊息應答功能,每個AMQP訊息頭中有一個Reply_to欄位,透過該欄位指定訊息返回到的佇列名稱(這是一個私有佇列)訊息的生產者可以監聽該欄位對應的佇列。

RabbitMQ-RPC

RabbitMQ叢集

RabbitMQ叢集的設計目標:

  1. 允許消費者和生產者在RabbitMQ節點崩潰的情況下繼續執行

  2. 能過透過新增節點來線性擴充套件訊息通訊吞吐量

從實際結果看,RabbitMQ完成設計目標上並不十分出色,主要原因在於預設的模式下,RabbitMQ的佇列例項子只存在在一個節點上(雖然後續也支援了映象佇列),既不能保證該節點崩潰的情況下佇列還可以繼續執行,也不能線性擴充套件該佇列的吞吐量。

叢集結構

RabbitMQ內部的後設資料主要有:

  1. 佇列後設資料-佇列名稱和屬性

  2. 交換器後設資料-交換器名稱,型別和屬性

  3. 繫結後設資料-路由資訊

雖然RabbitMQ的佇列實際只會在一個節點上,但後設資料可以存在各個節點上。舉個例子來說,當建立一個新的交換器時,RabbitMQ會把該資訊同步到所有節點上,這個時候客戶端不管連線的那個RabbitMQ節點,都可以訪問到這個新的交換器,也就能找到交換器下的佇列。

Cluster

如上圖所示,佇列A的例項實際只在一個RabbitMQ節點上,其它節點實際儲存的是隻想該佇列的指標。

為什麼RabbitMQ不在各個節點間做複製了,《RabbitMQ實戰》給出了兩個原因:

  1. 儲存成本-RabbitMQ作為記憶體佇列,複製對儲存空間的影響,畢竟記憶體是昂貴而有限的

  2. 效能損耗-釋出訊息需要將訊息複製到所有節點,特別是對於持久化佇列而言,效能的影響會很大

我理解成本這個原因並不完全成立,複製並不一定要複製到所有節點,比如一個佇列可以只做兩個副本,複製帶來的記憶體成本可以交給使用方來評估,畢竟在記憶體中沒有堆積的情況下,實際上佇列是不會佔用多大記憶體的。

還有一點是RabbitMQ本身並沒有保證訊息消費的有序性,所以實際上佇列被Partition到各個節點上,這樣才能真正達到線性擴容的目的(以RabbitMQ的現狀來說,單佇列實際是無法擴容的,只有在業務層做切分)。

注:RabbitMQ叢集中的節點可以是記憶體節點也可以是磁碟節點,但要求至少有一個磁碟節點,這樣出現故障時才能恢復資料。

映象佇列

映象佇列架構

RabbitMQ自己也考慮到了我們之前分析的單節點長時間故障無法恢復的問題,所以RabbitMQ 2.6.0之後它也支援了映象佇列,換個說法也就是副本。

Mirror

除了傳送訊息,所有的操作實際都在主複製上,從複製實際只是個冷備(預設的情況下所有RabbitMQ節點上都會有映象佇列的複製),如果使用訊息確認模式,RabbitMQ會在主複製和從複製都安全的接受到訊息時才通知生產者。

從這個結構上來看,如果從複製的節點掛了,實際沒有任何影響,如果主複製掛了,那麼會有一個從新選主的過程,這也是映象佇列的優點,除非所有節點都掛了,才會導致訊息丟失。重新選主後,RabbitMQ會給消費者一個消費者取消通知(Consumer Cancellation),讓消費者重連新的主複製。

映象佇列原理

1.RabbitMQ結構

BackingQueue

  • AMQPQueue:負責AMQP協議相關的訊息處理,包括接收訊息,投遞訊息,Confirm訊息等

  • BackingQueue:提供AMQQueue呼叫的介面,完成訊息的儲存和持久化工作

BackingQueue由Q1,Q2,Delta,Q3,Q4五個子佇列構成,在Backing中,訊息的生命週期有四個狀態:

  1. Alpha:訊息的內容和訊息索引都在RAM中。(Q1,Q4)

  2. Beta:訊息的內容儲存在Disk上,訊息索引儲存在RAM中。(Q2,Q3)

  3. Gamma:訊息的內容儲存在Disk上,訊息索引在DISK和RAM上都有。(Q2,Q3)

  4. Delta:訊息內容和索引都在Disk上。(Delta)

這裡以持久化訊息為例(可以看到非持久化訊息的生命週期會簡單很多),從Q1到Q4,訊息實際經歷了一個RAM->DISK->RAM這樣的過程,BackingQueue這麼設計的目的有點類似於Linux的Swap,當佇列負載很高時,透過將部分訊息放到磁碟上來節省記憶體空間,當負載降低時,訊息又從磁碟迴到記憶體中,讓整個佇列有很好的彈性。 因此觸發訊息流動的主要因素是:1.訊息被消費;2.記憶體不足。

RabbitMQ會更具訊息的傳輸速度來計算當前記憶體中允許儲存的最大訊息數量(Traget_RAM_Count),當: 記憶體中儲存的訊息數量+等待ACK的訊息數量>Target_RAM_Count  時,RabbitMQ才會把訊息寫到磁碟上,所以說雖然理論上訊息會按照Q1->Q2->Delta->Q3->Q4的順序流動,但是並不是每條訊息都會經歷所有的子佇列以及對應的生命週期。

從RabbitMQ的Backing Queue結構來看,當內部不足時,訊息要經歷多個生命週期,在Disk和RAM之間置換,者實際會降低RabbitMQ的處理效能(後續的流控就是關聯的解決方法)。

2.映象佇列結構

Mirror-Arch

所有對映象佇列主複製的操作,都會透過Guarented Multicasting(GM)同步到各個Salve節點,Coodinator負責組播結果的確認。

GM是一種可靠的組播通訊協議,保證組組內的存活節點都收到訊息。

GM

GM的主播並不是由Master節點來負責通知所有Slave的(目的是為了避免Master壓力過大,同時避免Master失效導致訊息無法最終Ack),RabbitMQ把一個映象佇列的所有節點組成一個連結串列,由主複製發起,由主複製最終確認通知到了所有的Slave,而中間由Slave接力的方式進行訊息傳播。

從這個結構來看,訊息完成整個映象佇列的同步耗時理論上是不低的,但是由於RabbitMQ訊息的訊息確認本身是非同步的模式,所以整體的吞吐量並不會受到太大影響。

流控

當RabbitMQ出現記憶體(預設是0.4)或者磁碟資源達到閾值時,會觸發流控機制,阻塞Producer的Connection,讓生產者不能繼續傳送訊息,直到記憶體或者磁碟資源得到釋放。

RabbitMQ基於Erlang/OTP開發,一個訊息的生命週期中,會涉及多個程式間的轉發,這些Erlang程式之間不共享記憶體,每個程式都有自己獨立的記憶體空間,如果沒有合適的流控機制,可能會導致某個程式佔用記憶體過大,導致OOM。因此,要保證各個程式佔用的內容在一個合理的範圍,RabbitMQ的流控採用了一種信用證機制(Credit),為每個程式維護了四類鍵值對:

  1. {credit_from,From}-該值表示還能向訊息接收程式From傳送多少條訊息

  2. {credit_to,To}-表示當前程式再接收多少條訊息,就要向訊息傳送程式增加Credit數量

  3. credit_blocked-表示當前程式被哪些程式block了,比如程式A向B傳送訊息,那麼當A的程式字典中{credit_from,B}的值為0是,那麼A的credit_blocked值為[B]

  4. credit_deferred-訊息接收程式向訊息傳送程式增加Credit的訊息列表,當程式被Block時會記錄訊息資訊,Unblock後依次傳送這些訊息

FlowControl

如圖所示,A程式當前可以傳送給B的訊息有100條,每發一次,值減1,直到為0,A才會被Block住。B消費訊息後,會給A增加新的Credit,這樣A才可以持續的傳送訊息。這裡只畫了兩個程式,多程式串聯的情況下,這中影響也就是從底向上傳遞的。

想學習Java工程化、分散式架構、 高併發、高效能、 深入淺出、 微服務架構、Spring,MyBatis,Netty原始碼分析等技術可以加群:479499375, 群裡有阿里大牛直播講解技術,以及Java大型網際網路技術的影片免費分享給大家,歡迎進群一起深入交流學習。

總結

注:本文基於的RabbitMQ材料可能較為陳舊,新的RabbitMQ可能會有不同的功能特性

整體來看,RabbitMQ的功能比較豐富(可惜沒有看到延遲,優先順序等功能),更適用於偏實時的業務場景,與Kafka這樣的佇列定位上有明顯的區別。它本身應該是一個簡單健壯的元件,但如果要應用在一個大規模的分散式系統中,實際還是需要做一些外部的再次開發,以解決我們前面提到的佇列儲存單點,流控等問題。直觀上看它的運維成本是會比較高的,需要使用方有一定的經驗。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31545684/viewspace-2284368/,如需轉載,請註明出處,否則將追究法律責任。

相關文章