理解AMQP協議和RabbitMQ的性能和可靠平衡

前言

在之前的博客中,已經使用Pika包實踐操作過RabbitMQ了,借用了幾個不同的Exchange實現不同功能的生產-消費模式,但是對RabbitMQ的細節還缺乏更進一步的理解。今天從AMQP協議起更仔細地來看一下MQ背後的實現。

AMQP協議

RabbitMQ通過AMQP協議通信,這就類似於HTTP客戶端和服務器進行通信一樣。
在AMQP中,客戶端和服務器之間的通信數據是拆成幀(frame)的結構。

對話啓動

需要對話首先要建立連接:
在這裏插入圖片描述
客戶端先發送協議頭(protocol header)給服務器,服務器收到後,返回Connection.Start給客戶端,客戶端確認後返回Connection.StartOk給服務器,完成回話啓動。

信道

AMQP規範定義了通信的信道,一個AMQP連接可以有多個信道,允許客戶端和服務器之間進行多次會話。

AMQP幀結構

上面留意到,建立連接時服務器和客戶端的相應都有共同部分Connection,因爲AMQP命令是分爲類和方法,用點(.)連接。連接時,Connection是使用的類,StartStartOk是方法。

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上來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章