前言
在之前的博客中,已經使用Pika包實踐操作過RabbitMQ了,借用了幾個不同的Exchange實現不同功能的生產-消費模式,但是對RabbitMQ的細節還缺乏更進一步的理解。今天從AMQP協議起更仔細地來看一下MQ背後的實現。
AMQP協議
RabbitMQ通過AMQP協議通信,這就類似於HTTP客戶端和服務器進行通信一樣。
在AMQP中,客戶端和服務器之間的通信數據是拆成幀(frame)的結構。
對話啓動
需要對話首先要建立連接:
客戶端先發送協議頭(protocol header)給服務器,服務器收到後,返回Connection.Start
給客戶端,客戶端確認後返回Connection.StartOk
給服務器,完成回話啓動。
信道
AMQP規範定義了通信的信道,一個AMQP連接可以有多個信道,允許客戶端和服務器之間進行多次會話。
AMQP幀結構
上面留意到,建立連接時服務器和客戶端的相應都有共同部分Connection
,因爲AMQP命令是分爲類和方法,用點(.)連接。連接時,Connection
是使用的類,Start
和StartOk
是方法。
AMQB的幀由以下組件組成:
- 幀類型
- 信道
- 幀大小(字節)
- 幀有效載荷
- 結束字節標記(0xce)
AMQP的幀有五種類型: - 協議頭幀,也就是上面建立連接使用,僅使用一次
- 方法幀,攜帶發送或接收的請求或相應
- 內容頭幀,消息的大小和屬性
- 消息體幀,消息的內容
- 心跳幀,雙向均可發送,確保連接兩端可用和正常工作
下面來看一下這幾種類型的幀如何組成消息。
-
除了建立連接以外,AMQP在通信時,首先使用方法幀構建RPC請求所需的類、方法和參數。按照上文的幀結構,現在構造一個幀:
- 幀類型爲方法幀(1)
- 信道0
- 有效載荷大小爲41
- 有效載荷爲類、方法、參數等
- 以0xce結尾
-
方法幀通知對方後,繼續構造一個內容頭幀,告知對方接下來要發送的消息大小和屬性:
- 幀類型爲內容頭幀(2)
- 信道1
- 有效載荷大小爲45
- 有效載荷爲:消息體大小55,被設置的屬性爲144(內容類型)和200(app_id),被設置的屬性的值分別爲application/json和Test,timestamp屬性爲1014206880,投遞模式爲1
- 以0xce結尾
注意內容頭幀聲明的這些屬性是在BasicProperty映射表中的。
-
內容頭幀通知對方後,繼續構造一個消息體幀發送具體消息:
- 幀類型爲消息體幀(3)
- 信道爲1
- 有效載荷大小爲55(對應內容頭幀中的55)
- 有效載荷爲一段JSON格式的字符串
- 以0xce結尾
注意AMQP協議是不會理會消息中的內容的,不對消息進行解析,即使知道對方是JSON格式內容。
使用AMQP協議
瞭解完AMQP協議的格式後,來看一下如何使用AMQP協議。
首先,需要聲明一個交換器(Exchange)。交換器在AMQP規範中有自己的類,使用Exchange.Declear命令創建交換器,服務端使用Exchange.DeclearOk進行響應:
然後再創建一個隊列(Queue)。同樣Queue.Declear和Queue.DeclearOk完成。注意聲明隊列的時候多次發送同一個Queue.Declear不會有作用,只有第一次Declear會被處理,後續再Declear同樣內容無效,Declear同名不同屬性隊列也無效。
現在我們有交換器和隊列,在之前的博客中我們知道,消息是發送給Exchange的,然後Exchange推送至隊列中。Exchange和Queue的關係需要進行綁定。使用Queue.Bind和Queue.BindOk命令將Queue綁定至Exchange。
現在所有準備工作都完成了,我們來發布消息到RabbitMQ。通過上文可知,發送消息需要發送方法幀、內容頭幀和(至少一個)消息體幀。其中方法幀在發佈消息時應該是對應Basic類的Publish方法。
當RabbitMQ收到消息後,它會嘗試將方法幀中的交換器名稱和配置交換器的數據庫進行匹配。如果配置中不存在交換器,將會自動丟棄該消息。如果希望確保投遞消息成功,發佈時mandatory標誌需要設置爲true,或者使用投遞確認機制。
RabbitMQ收到的消息將會以FIFO的順序放入隊列,並且放入隊列的是消息的引用而不是消息本身,這樣可以允許一個消息放入多個隊列中。
RabbitMQ可以將這些消息保存在內存中或寫入磁盤,取決於Basic.Properties中指定的delivery-mode屬性。
再來看一下如何從RabbitMQ中消費消息。
與Basic.Publish類似,首先客戶端發送Basic.Consume命令,服務端返回Basic.ConsumeOk,消費者進入活躍狀態。然後服務端開始向消費者發送消息,以Basic.Deliver爲方法幀,加上內容頭和消息體幀發送消息。直到消費者發送Basic.Cancel或者觸發一些事件前,服務端都會一直髮送消息。
在發送Basic.Consume時,可以設置no_ack=false,這樣消費者必須對每條消息發送Basic.Ack進行確認,否則RabbitMQ就會連續發送消息直到Basic.Cancel。
當發送Basic.Ack相應幀的時候,消費者必須在Basic.Deliver方法幀中傳遞一個投遞標籤(delivery tag)的參數。
AMQP的Basic.Properties
在內容頭幀中,有包含很多消息屬性,如上文提到的屬性144,值爲application/json,實際上屬性144就是代表content-type。通過這些屬性來對消息體進行描述。來看一下Basic.Properties都有哪些屬性:
本文不打算一一解釋各個屬性,它們在需要使用時都可以通過文檔查詢到。下面選取幾個常見的屬性簡單介紹。
- expiration,時間戳,超過後消息會被服務器丟棄。
- delivery-mode,1表示非持久化消息,2表示持久化消息,性能相關。
- header,自定義消息頭,值爲鍵值對,通過header屬性可以結合header類型的Exchange實現自定義的消息路由。
- priority,優先級,如果存在更高優先級的消息,消費者將更早獲取到。
消息發佈的性能權衡
《深入RabbitMQ》中有一幅圖簡單描述了RabbitMQ實現高性能和可靠投遞時的設置組合:
通過結合不同的組合,我們可以從RabbitMQ上榨取最好的性能或者保障更可靠的消息傳遞。
下面介紹幾個實現不同需求的設置。
mandatory
mandatory標誌是和Basic.Publish一起傳遞的參數,告訴RabbitMQ如果消息不可路由,將它通過Basic.Return返回給消費者。
發佈者確認替代事務
爲了確認RabbitMQ收到消息,在發送消息前,發送Confirm.Select命令,等待RabbitMQ返回Confirm.SelectOk以開啓投遞確認。開啓後,對於每條發佈的消息,服務器都會返回Basic.Ack響應,或者Baskc.Nack並讓發佈者決定如何處理。
備用交換器處理無法路由的消息
聲明一個Exchange作爲備用交換器,然後在聲明其他交換器時使用參數alternate-exchange=備用交換器
。備用交換器(AE)類型設定爲fanout,當消息在Exchange上無法路由時,它將會由AE路由至死信隊列。
事務
在沒有確認投遞(Confirm.Select)的情況下,事務是確保消息被成功投遞的唯一方法。AMQP事務(TX)的使用是:
- 發送TX.Select,相應TX.SelectOk
- Basic.Publish
- TX.Commit和TX.CommitOk
在Basic.Publish後如果有異常,可以通過TX.Rollback處理。
HA隊列
HA隊列作爲RabbitMQ的高可用實現,通過RabbitMQ集羣,在創建Queue時設置HA策略,開啓HA隊列。當消息發佈到高可用隊列中,該消息會被髮送到集羣中的每臺服務器,一旦消息在任何節點完成消費,那麼消息的所有副本將立即從其他節點中刪除。
delivery-mode
通過設置delivery-mode=2,消息會被持久化到硬盤。持久化會導致性能問題。當消息引用不存在任何隊列中,RabbitMQ將從硬盤中刪除消息。
RabbitMQ回推
發佈者有可能大量發送消息,如果不進行處理,有可能會拖垮服務。
在舊版本中,發佈者發佈過快,將會收到一條Channel.Flow讓發佈者產生阻塞,直到接收到另一條Channel.Flow命令爲止。
但是對於不禮貌的發佈者而言,無視Channel.Flow命令繼續發送仍然會導致問題。RabbitMQ團隊使用TCP背壓機制來解決這個問題,通過停止接受TCP的低層數據來防止被拖垮。
在內部RabbitMQ有一套信用機制,接收到消息時會扣除一點信用值,完成處理返還信用值。當信用值不足時,當前連接的消息會被跳過直到它有足夠的信用值爲止。
RabbitMQ還有通知客戶端已被阻塞的方法:Connection.Blocked和Connection.Unblocked。
RabbitMQ和消費者
上面聊完發佈者和RabbitMQ,現在輪到消費者和RabbitMQ了。
拉取和消費
消費者獲取消息可以通過Basic.Get和Basic.Consume,下面來比較一下這兩者:
- Basic.Get類似於輪詢,如果有消息可消費,返回Basic.GetOk和內容頭、消息體;如果沒有消息可消費,返回Basic.GetEmpty。
- Basic.Consume開啓消費者活動狀態,RabbitMQ如果有消息即可向消費者進行推送:Basic.Deliver,視情況消費者再返回Basic.Ack。
- Basic.Consume的性能比Basic.Get更好,Get的輪詢影響吞吐量,並且它不知道什麼時候會有新的消息,所以要一直詢問。
no-ack
消費者發送Basic.Consume的時候,可以帶上no-ack標誌,表示消費消息不需要進行ack確認,提高性能。
如果不開啓no-ack,RabbitMQ會等待消費者發送Basic.Ack確認消息,如果不得到確認,消息將不會被消費掉。
服務質量設置控制消費者預取
如果消息要一條一條確認,那會比較麻煩。通過QoS設置,在確認消息之前,消費者可以預先接收一定數量的消息。
使用QoS的好處就是不用每次都確認消息,通過Basic.Ack設置multiple屬性爲True,可以讓RabbitMQ知道消費者想確認之前未確認的消息。
消費者使用事務
和生產者一樣使用TX類,可能會對性能有影響。
拒絕消息
Basic.Reject命令告知服務端,消費者無法對投遞的消息進行處理。類似的還有RabbitMQ團隊擴展的Basic.Nack命令,與Basic.Reject功能類似,但是支持像Basic.Ack一樣對多消息進行處理。
死信交換器(DLX)是對AMQP規範的擴展。DLX是用來保存被拒絕的消息。一旦拒絕了一個不重新發送的消息,RabbitMQ將把消息路由到隊列的x-dead-letter-exchange
參數中指定的交換器。
使用DLX,首先需要聲明一個Exchange(圖中的x),在聲明Queue的時候將Queue的x-dead-letter-exchange
指定爲x即可。
控制隊列
在RabbitMQ可以定義很多不同的隊列行爲,如:
- 自動銷燬自己
- 只允許一個消費者消費
- 消息自動過期
- 保持消息數量有限
- 將舊消息推出堆棧
臨時隊列
有的時候我們會希望在沒有消費者連接隊列時,自動刪除這個隊列。創建自動刪除隊列非常簡單,只需要在Queue.Declear中將auto_delete
標誌設置爲True。
只允許單個消費者
RabbitMQ鼓勵多個消費者進行消費,當然它也還是可以支持消費者獨佔隊列的。通過設置exclusive
屬性爲True可以確保只有單個消費者進行消費,隊列會在消費者斷開連接後自動刪除。
自動過期隊列
之前有提到過的消息的expiration
參數,現在通過設置x-expires
參數,可以聲明一個自動過期隊列。不過需要注意,自動過期隊列只有在沒有消費者的情況下才會過期,否則只有在發出了Basic.Cancel之後纔會自動刪除。如果隊列在TTL內收到了Basic.Get請求,那麼隊列的過期設置會無效。
永久隊列
如果需要重啓後仍可用的隊列,需要在聲明時設置durable
爲True。務必要將隊列持久化和消息持久化區分開來,相對應的,消息持久化是delivery-mode
設置爲2。當隊列持久化設置之後,需要通過Queue.Delete刪除。
隊列中的消息自動過期
對於不重要的消息,可以在沒被消費的情況下,不需要存在太久。對於隊列而言,設置x-message-ttl
可以規定隊列中的所有消息最大生存時間。
隊列最大長度
從RabbitMQ 3.1.0開始,可以指定隊列的最大長度,超過最大值時,添加新消息的同時就會刪除位於隊列最前端的消息,也就是確保隊列中爲最近新增的n個消息。通過設置x-max-length
參數可以實現這個功能。
RabbitMQ消息路由模式
消息路由在之前的博客中已經介紹過了,RabbitMQ中主要有幾種基本的Exchange:
- Direct,匹配routing_key
- Fanout,廣播至所有Queue
- Topic,模式匹配routing_key
- Headers
其中Headers之前的博客是沒有使用過的,在這裏簡略介紹一下。
之前提到過,在Basic.Publish時可以給消息添加各種headers屬性,就像HTTP的請求頭字段一樣。Headers的Exchange通過設定一些header字段,如果消息的header能夠匹配Exchange的header,則可以發佈到對應的Queue中去。簡單來說,就是通過headers來匹配路由的方式。
總結
本文主要對AMQP協議進行了介紹,同時協議(以及RabbitMQ自行擴展的規範)中的各個設置可能會對MQ的性能和可靠性產生影響,這些內容也從發佈者和消費者的角度進行了介紹,以滿足不同性能、可靠性要求的業務。
深入瞭解RabbitMQ,很顯然真正的消息隊列應用與簡單的Radis用作消息隊列差異是非常大的,RabbitMQ實現了很多簡單隊列原生不支持的功能,例如優先級隊列、自銷燬隊列、隊列可靠性保障、拉取與消費模式等。在消息隊列的場景中,如果有可靠性的要求,應該避免再使用自建的簡單隊列和造輪子再保障SLA,將對應業務轉移至專業的MQ上來。