rabbitmq訊息佇列原理

vipshop_fin_dev發表於2018-08-12

一、rabbitmq架構

RabbitMQ是一個流行的開源訊息佇列系統,是AMQP(高階訊息佇列協議)標準的實現,由以高效能、健壯、可伸縮性出名的Erlang語言開發,並繼承了這些優點。rabbitmq簡單架構如下:
這裡寫圖片描述
上圖簡單展示了rabbitmq的架構,從圖中看到幾個關鍵字:vhost、exchange、route key、queue等,後面會介紹這些概念。

下面看下rabbitmq的程式模型:
這裡寫圖片描述
看到這個圖,相信大家應該很熟悉,沒錯就是事件驅動模型(或者說反應堆模型),這是一種高效能的非阻塞io執行緒模型,不過在Erlang中稱為程式模型。

tcp_acceptor程式接收客戶端連線,建立rabbit_reader、rabbit_writer、rabbit_channel程式。
rabbit_reader接收客戶端連線,解析AMQP幀;rabbit_writer向客戶端返回資料;
rabbit_channel解析AMQP方法,對訊息進行路由,然後發給相應佇列程式。
rabbit_amqqueue_process是佇列程式,在RabbitMQ啟動(恢復durable型別佇列)或建立佇列時建立。
rabbit_msg_store是負責訊息持久化的程式。

在整個系統中,存在一個tcp_accepter程式,一個rabbit_msg_store程式,有多少個佇列就有多少個rabbit_amqqueue_process程式,每個客戶端連線對應一個rabbit_reader和rabbit_writer程式。
二、關於AMQP協議

1.AMQP幀元件

AMQP幀由五個不同的元件組成:

幀型別
通道編號
以位元組為單位的幀大小
幀有效載荷payload
結束位元組標誌(ASCII值206)

這裡寫圖片描述
2.幀型別

AMQP規範定義了五種型別的幀:協議頭幀、方法幀、內容幀、訊息體幀及心跳幀。每種幀型別都有明確的目的,有些幀的使用頻率比其他的高很多:

協議頭幀用於連線到rabbitmq,進使用一次。
方法幀攜帶傳送給rabbitmq或者從rabbitmq接收到的rpc請求或者響應
內容頭包含一條訊息的大小和屬性。
訊息體幀包含訊息的內容
心跳幀在客戶端與rabbitmq直接進行傳遞,作為一種校驗機制確保連線的兩端都可用並且正常工作。

3.將訊息編組成幀

我們使用方法幀、內容頭幀和訊息體幀組成一個完整的rabbitmq訊息。方法頭幀攜帶命令和執行它所需要的引數(如交換器和路由鍵)、內容幀包含訊息的基本屬性以及訊息的大小,訊息體幀也就是攜帶我們真正需要傳送的訊息內容。
這裡寫圖片描述
4.方法幀結構
這裡寫圖片描述
5.內容頭幀結構
這裡寫圖片描述
內容頭包含的具體屬性如下:
這裡寫圖片描述
content-type:訊息體的報文編碼,如application/json
expiration:訊息過期時間
reply-to:響應訊息的佇列名
content-encoding:報文壓縮的編碼,如gzip
message-id:訊息的編號
correlation-id:鏈路id
deliver-mode:告訴rabbitmq將訊息寫入磁碟還是記憶體
user-id:投遞訊息的使用者(傳送訊息時不要設定該值)
timestamp:投遞訊息的時間
headers:定義一些屬性,可用於實現rabbitmq路由(比如exchange型別是headers的時候用到)

6.訊息體幀結構
這裡寫圖片描述
7.幾個概念:
Broker:簡單來說就是訊息佇列伺服器實體
Exchange:訊息交換機,它指定訊息按什麼規則,路由到哪個佇列
Queue:訊息佇列載體,每個訊息都會被投入到一個或多個佇列,佇列型別又分為臨時佇列,持久化佇列,排他佇列
Binding:繫結,它的作用就是把exchange和queue按照路由規則繫結起來
Routing Key:路由關鍵字,exchange根據這個關鍵字進行訊息投遞
vhost:虛擬主機,一個broker裡可以開設多個vhost,用作不同使用者的許可權分離
producer:訊息生產者,就是投遞訊息的程式
consumer:訊息消費者,就是接受訊息的程式
channel:訊息通道,在客戶端的每個連線裡,可建立多個channel,每個channel代表一個會話任務

四、通訊過程
1.啟動會話
這裡寫圖片描述
2.宣告交換器
這裡寫圖片描述
3.宣告佇列
這裡寫圖片描述
4.繫結佇列到exchange
這裡寫圖片描述
5.傳送訊息-使用事務機制
這裡寫圖片描述
對事務的支援是AMQP協議的一個重要特性。假設當生產者將一個持久化訊息傳送給伺服器時,假如使用no_ack模式,所以即使伺服器崩潰,沒有持久化該訊息,生產者也無法獲知該訊息已經丟失。如果此時使用事務,即通過txSelect()開啟一個事務,然後傳送訊息給伺服器,然後通過txCommit()提交該事務,即可以保證,如果txCommit()提交了,則該訊息一定會持久化,如果txCommit()還未提交即伺服器崩潰,則該訊息不會伺服器就收。當然Rabbit MQ也提供了txRollback()命令用於回滾某一個事務。但是使用事務,會導致效能下降,它使得生產者釋出訊息後必須等到訊息真正持久化後服務端響應了才結束本次連線,所以需要在實際應用中平衡效能與安全的問題。

6.傳送訊息-非事務方式
這裡寫圖片描述
使用事務固然可以保證只有提交的事務,才會被伺服器執行。但是這樣同時也將客戶端與訊息伺服器同步起來,這背離了訊息佇列解耦的本質。Rabbit MQ提供了一個更加輕量級的機制來保證生產者可以感知伺服器訊息是否已被路由到正確的佇列中——Confirm。如果設定channel為confirm狀態,則通過該channel傳送的訊息都會被分配一個唯一的ID,然後一旦該訊息被正確的路由到匹配的佇列中後,伺服器會返回給生產者一個Confirm,該Confirm包含該訊息的ID,這樣生產者就會知道該訊息已被正確分發。對於持久化訊息,只有該訊息被持久化後,才會返回Confirm。

Confirm機制的最大優點在於非同步,生產者在傳送訊息以後,即可繼續執行其他任務(也就是非同步監聽服務端的ack即可)。而伺服器返回Confirm後,會觸發生產者的回撥函式,生產者在回撥函式中處理Confirm資訊。如果訊息伺服器發生異常,導致該訊息丟失,會返回給生產者一個nack,表示訊息已經丟失,這樣生產者就可以通過重發訊息,保證訊息不丟失。Confirm機制在效能上要比事務優越很多。

但是Confirm機制,無法進行回滾,就是一旦伺服器崩潰,生產者無法得到Confirm資訊,生產者其實本身也不知道該訊息吃否已經被持久化,只有繼續重發來保證訊息不丟失,但是如果原先已經持久化的訊息,並不會被回滾,這樣佇列中就會存在兩條相同的訊息,系統需要支援去重。

7.消費訊息
這裡寫圖片描述

五、使用delivery-mode平衡速度和安全
delivery-mode有兩個值:1表示非持久化,2表示持久化訊息

1.傳送訊息到純記憶體佇列中,delivery-mode = 1
這裡寫圖片描述
特點:非持久化的訊息在服務當機的時候會丟失資料,但是由於不需要磁碟io,儘可能地降低訊息投遞的延遲性,效能較高。

2.釋出訊息到支援磁碟儲存的佇列,delivery-mode = 2
這裡寫圖片描述
特點:持久化的訊息安全性較高,儘管服務當機,資料也不會丟失,但是在投遞訊息的過程中需要發生磁碟io,效能相對純記憶體投遞的方式低,但是儘管是產生了磁碟io,由於日誌的記錄方式是直接追加到訊息日誌檔案的末尾,屬於順序io,沒有隨機io,所以效能還是可以接受的。

  1. 大概原理:
    所有佇列中的訊息都以append的方式寫到一個檔案中,當這個檔案的大小超過指定的限制大小後,關閉這個檔案再建立一個新的檔案供訊息的寫入。檔名(*.rdq)從0開始然後依次累加。當某個訊息被刪除時,並不立即從檔案中刪除相關資訊,而是做一些記錄,當垃圾資料達到一定比例時,啟動垃圾回收處理,將邏輯相鄰的檔案中的資料合併到一個檔案中。

  2. 訊息的讀寫及刪除:
    rabbitmq在啟動時會建立msg_store_persistent,msg_store_transient兩個程式,一個用於持久訊息的儲存,一個用於記憶體不夠時,將儲存在記憶體中的非持久化資料轉存到磁碟中。所有佇列的訊息的寫入和刪除最終都由這兩個程式負責處理,而訊息的讀取則可能是佇列本身直接開啟檔案進行讀取,也可能是傳送請求由msg_store_persisteng/msg_store_transient程式進行處理。

在進行訊息的儲存時,rabbitmq會在ets表中記錄訊息在檔案中的對映,以及檔案的相關資訊。訊息讀取時,根據訊息ID找到該訊息所儲存的檔案,在檔案中的偏移量,然後開啟檔案進行讀取。訊息的刪除只是從ets表刪除指定訊息的相關資訊,同時更新訊息對應儲存的檔案的相關資訊(更新檔案有效資料大小)。
六、訊息路由模式
1.fanout模式
fanout型別的Exchange路由規則非常簡單,它會把所有傳送到該Exchange的訊息路由到所有與它繫結的Queue中。
這裡寫圖片描述
上圖中,生產者傳送到Exchange的所有訊息都會路由到圖中的兩個Queue,並最終被兩個消費者(C1與C2)消費。
2.direct模式
direct型別的Exchange路由規則也很簡單,它會把訊息路由到那些binding key與routing key完全匹配的Queue中。如圖,生產者傳送訊息的routing key=key1的時候,只有繫結了key1的queue才能收到資訊
這裡寫圖片描述
3.topic模式
topic型別的Exchange在匹配規則上進行了擴充套件,它與direct型別的Exchage相似,也是將訊息路由到binding key與routing key相匹配的Queue中,但這裡的匹配規則有些不同,它約定:
routing key為一個句點號“. ”分隔的字串(我們將被句點號“. ”分隔開的每一段獨立的字串稱為一個單詞),如“image.new.profile”.
binding key與routing key一樣也是句點號“. ”分隔的字串
binding key中可以存在兩種特殊字元“”與“#”,用於做模糊匹配,其中“”用於匹配下一個據點前的所有字元,“#”用於匹配所有字元,包括句點(可以是零個)
這裡寫圖片描述
如圖,生產者以routing key為image.new.profile釋出訊息,這key可以被image.*.profile以及image.#匹配到,所有這兩個佇列都可以收到訊息。由此可見,topic的路由方式更加靈活。
3.headers模式
headers型別的Exchange不依賴於routing key與binding key的匹配規則來路由訊息,而是根據傳送的訊息內容中的headers屬性進行匹配。
在繫結Queue與Exchange時指定一組鍵值對以及x-match引數,x-match引數是字串型別,可以設定為any或者all。如果設定為any,意思就是隻要匹配到了headers表中的任何一對鍵值即可,all則代表需要全部匹配。

七、rabbitmq流量控制

RabbitMQ可以對記憶體和磁碟使用量設定閾值,當達到閾值後,生產者將被阻塞(block),直到對應項恢復正常。除了這兩個閾值,RabbitMQ在正常情況下還用流控(Flow Control)機制來確保穩定性。
Erlang程式之間並不共享記憶體(binaries型別除外),而是通過訊息傳遞來通訊,每個程式都有自己的程式郵箱。Erlang預設沒有對程式郵箱大小設限制,所以當有大量訊息持續發往某個程式時,會導致該程式郵箱過大,最終記憶體溢位並崩潰。
在RabbitMQ中,如果生產者持續高速傳送,而消費者消費速度較低時,如果沒有流控,很快就會使內部程式郵箱大小達到記憶體閾值,阻塞生產者(得益於block機制,並不會崩潰)。然後RabbitMQ會進行page操作,將記憶體中的資料持久化到磁碟中。
為了解決該問題,RabbitMQ使用了一種基於信用證的流控機制。訊息處理程式有一個信用組{InitialCredit,MoreCreditAfter},預設值為{200, 50}。訊息傳送者程式A向接收者程式B發訊息,每發一條訊息,Credit數量減1,直到為0,A被block住;對於接收者B,每接收MoreCreditAfter條訊息,會向A傳送一條訊息,給予A MoreCreditAfter個Credit,當A的Credit>0時,A可以繼續向B傳送訊息。

八、 RabbitMQ 多層訊息佇列

RabbitMQ完全實現了AMQP協議,類似於一個郵箱服務。Exchange負責根據ExchangeType和RoutingKey將訊息投遞到對應的訊息佇列中,訊息佇列負責在消費者獲取訊息前暫存訊息。在RabbitMQ中,MessageQueue主要由兩部分組成,一個為AMQQueue,主要負責實現AMQP協議的邏輯功能。另外一個是用來儲存訊息的BackingQueue。
為了高效處理入隊和出隊的訊息、避免不必要的磁碟IO,BackingQueue程式為訊息設計了4種狀態和5個內部佇列。
(1) 4種狀態包括:

alpha,訊息的內容和索引都在記憶體中;
beta,訊息的內容在磁碟,索引在記憶體;
gamma,訊息的內容在磁碟,索引在磁碟和記憶體中都有;
delta,訊息的內容和索引都在磁碟。

對於持久化訊息,RabbitMQ先將訊息的內容和索引儲存在磁碟中,然後才處於上面的某種狀態(即只可能處於alpha、gamma、delta三種狀態之一)。
(2) 5個內部佇列

包括:q1、q2、delta、q3、q4。q1和q4佇列中只有alpha狀態的訊息;q2和q3包含beta和gamma狀態的訊息;delta佇列是訊息按序存檔後的一種邏輯佇列,只有delta狀態的訊息。所以delta佇列並不在記憶體中,其他4個佇列則是由erlang queue模組實現。
這裡寫圖片描述
訊息從q1入隊,q4出隊,在內部佇列中傳遞的過程一般是經q1順序到q4。實際執行並非必然如此:開始時所有佇列都為空,訊息直接進入q4(沒有訊息堆積時);記憶體緊張時將q4隊尾部分訊息轉入q3,進而再由q3轉入delta,此時新來的訊息將存入q1(有訊息堆積時)。

當記憶體緊張時觸發paging,paging將大量alpha狀態的訊息轉換為beta和gamma;如果記憶體依然緊張,繼續將beta和gamma狀態轉換為delta狀態。Paging是一個持續過程,涉及到大量訊息的多種狀態轉換,所以Paging的開銷較大,嚴重影響系統效能。

九、高可用佇列(HA)
在生產環境下,一般都不會允許rabbitmq這種訊息中介軟體單點,以免單點故障導致服務不可用,那麼rabbitmq同樣可以叢集部署來保證服務的可用性,在rabbitmq叢集中,我們可以定義HA佇列,可以在web管理平臺設定,也可以通過AMQP介面設定,當我們定義某個HA佇列的時候,會在叢集的各個節點上都建立該佇列,釋出訊息的時候,直接傳送至master服務,當master服務受到訊息後,把訊息同步至各個從節點,假如開啟事務的情況下,是需要在訊息被同步到各個節點之後才算完成事務,所以會帶來一定的效能損耗,所以還是回到之前說的,效能和安全直接,需要根據實際業務的需要找到平衡點。
這裡寫圖片描述
當master服務當機之後,其中一個slaver節點會升級為master,訊息不會丟失(因為已經完成了事務的訊息都會在各個節點有備份)
ha-佇列可以跨越叢集的每臺服務,或者僅使用其中一批獨立節點。如果是全部節點都為副本的時候,將x-ha-policy引數設定為all,否則設定為nodes,然後在設定另一個引數:x-ha-nodes,該引數指定ha佇列所在的節點列表。思考下,rabbitmq的叢集節點是不是越多越好?

相關文章