1. 程式人生 > >AMQP協議簡介(源自官網的翻譯)

AMQP協議簡介(源自官網的翻譯)

對AMQP有了更深刻的認識。

AMQP 0-9-1 簡介

AMQP 0-9-1 和 AMQP 模型高階概述

AMQP是什麼

AMQP(高階訊息佇列協議)是一個網路協議。它支援符合要求的客戶端應用(application)和訊息中介軟體代理(messaging middleware broker)之間進行通訊。

訊息代理和他們所扮演的角色

訊息代理(message brokers)從釋出者(publishers)亦稱生產者(producers)那兒接收訊息,並根據既定的路由規則把接收到的訊息傳送給處理訊息的消費者(consumers)。

由於AMQP是一個網路協議,所以這個過程中的釋出者,消費者,訊息代理 可以存在於不同的裝置上。

AMQP 0-9-1 模型簡介

AMQP 0-9-1的工作過程如下圖:訊息(message)被髮布者(publisher)傳送給交換機(exchange),交換機常常被比喻成郵局或者郵箱。然後交換機將收到的訊息根據路由規則分發給繫結的佇列(queue)。最後AMQP代理會將訊息投遞給訂閱了此佇列的消費者,或者消費者按照需求自行獲取。


釋出者(publisher)釋出訊息時可以給訊息指定各種訊息屬性(message meta-data)。有些屬性有可能會被訊息代理(brokers)使用,然而其他的屬性則是完全不透明的,它們只能被接收訊息的應用所使用。

從安全形度考慮,網路是不可靠的,接收訊息的應用也有可能在處理訊息的時候失敗。基於此原因,AMQP模組包含了一個訊息確認(message acknowledgements)的概念:當一個訊息從佇列中投遞給消費者後(consumer),消費者會通知一下訊息代理(broker),這個可以是自動的也可以由處理訊息的應用的開發者執行。當“訊息確認”被啟用的時候,訊息代理不會完全將訊息從佇列中刪除,直到它收到來自消費者的確認回執(acknowledgement)。

在某些情況下,例如當一個訊息無法被成功路由時,訊息或許會被返回給釋出者並被丟棄。或者,如果訊息代理執行了延期操作,訊息會被放入一個所謂的死信佇列中。此時,訊息釋出者可以選擇某些引數來處理這些特殊情況。

佇列,交換機和繫結統稱為AMQP實體(AMQP entities)。

AMQP是一個可編的程協議

AMQP 0-9-1是一個可程式設計協議,某種意義上說AMQP的實體和路由規則是由應用本身定義的,而不是由訊息代理定義。包括像宣告佇列和交換機,定義他們之間的繫結,訂閱佇列等等關於協議本身的操作。

這雖然能讓開發人員自由發揮,但也需要他們注意潛在的定義衝突。當然這在實踐中很少會發生,如果發生,會以配置錯誤(misconfiguration)的形式表現出來。

應用程式(Applications)宣告AMQP實體,定義需要的路由方案,或者刪除不再需要的AMQP實體。

交換機和交換機型別

交換機是用來發送訊息的AMQP實體。交換機拿到一個訊息之後將它路由給一個或零個佇列。它使用哪種路由演算法是由交換機型別和被稱作繫結(bindings)的規則所決定的。AMQP 0-9-1的代理提供了四種交換機

Name(交換機型別) Default pre-declared names(預宣告的預設名稱)
Direct exchange(直連交換機) (Empty string) and amq.direct
Fanout exchange(扇型交換機) amq.fanout
Topic exchange(主題交換機) amq.topic
Headers exchange(頭交換機) amq.match (and amq.headers in RabbitMQ)

除交換機型別外,在宣告交換機時還可以附帶許多其他的屬性,其中最重要的幾個分別是:

  • Name
  • Durability (訊息代理重啟後,交換機是否還存在)
  • Auto-delete (當所有與之繫結的訊息佇列都完成了對此交換機的使用後,刪掉它)
  • Arguments(依賴代理本身)

交換機可以有兩個狀態:持久(durable)、暫存(transient)。持久化的交換機會在訊息代理(broker)重啟後依舊存在,而暫存的交換機則不會(它們需要在代理再次上線後重新被宣告)。然而並不是所有的應用場景都需要持久化的交換機。

預設交換機

預設交換機(default exchange)實際上是一個由訊息代理預先宣告好的沒有名字(名字為空字串)的直連交換機(direct exchange)。它有一個特殊的屬性使得它對於簡單應用特別有用處:那就是每個新建佇列(queue)都會自動繫結到預設交換機上,繫結的路由鍵(routing key)名稱與佇列名稱相同。

舉個栗子:當你聲明瞭一個名為"search-indexing-online"的佇列,AMQP代理會自動將其繫結到預設交換機上,繫結(binding)的路由鍵名稱也是為"search-indexing-online"。因此,當攜帶著名為"search-indexing-online"的路由鍵的訊息被髮送到預設交換機的時候,此訊息會被預設交換機路由至名為"search-indexing-online"的佇列中。換句話說,預設交換機看起來貌似能夠直接將訊息投遞給佇列,儘管技術上並沒有做相關的操作。

直連交換機

直連型交換機(direct exchange)是根據訊息攜帶的路由鍵(routing key)將訊息投遞給對應佇列的。直連交換機用來處理訊息的單播路由(unicast routing)(儘管它也可以處理多播路由)。下邊介紹它是如何工作的:

  • 將一個佇列繫結到某個交換機上,同時賦予該繫結一個路由鍵(routing key)
  • 當一個攜帶著路由鍵為R的訊息被髮送給直連交換機時,交換機會把它路由給繫結值同樣為R的佇列。

直連交換機經常用來迴圈分發任務給多個工作者(workers)。當這樣做的時候,我們需要明白一點,在AMQP 0-9-1中,訊息的負載均衡是發生在消費者(consumer)之間的,而不是佇列(queue)之間。

直連型交換機圖例:

扇型交換機

扇型交換機(funout exchange)將訊息路由給繫結到它身上的所有佇列,而不理會繫結的路由鍵。如果N個佇列繫結到某個扇型交換機上,當有訊息傳送給此扇型交換機時,交換機會將訊息的拷貝分別傳送給這所有的N個佇列。扇型用來交換機處理訊息的廣播路由(broadcast routing)。

因為扇型交換機投遞訊息的拷貝到所有繫結到它的佇列,所以他的應用案例都極其相似:

  • 大規模多使用者線上(MMO)遊戲可以使用它來處理排行榜更新等全域性事件
  • 體育新聞網站可以用它來近乎實時地將比分更新分發給移動客戶端
  • 分發系統使用它來廣播各種狀態和配置更新
  • 在群聊的時候,它被用來分發訊息給參與群聊的使用者。(AMQP沒有內建presence的概念,因此XMPP可能會是個更好的選擇)

扇型交換機圖例:

主題交換機

主題交換機(topic exchanges)通過對訊息的路由鍵和佇列到交換機的繫結模式之間的匹配,將訊息路由給一個或多個佇列。主題交換機經常用來實現各種分發/訂閱模式及其變種。主題交換機通常用來實現訊息的多播路由(multicast routing)。

主題交換機擁有非常廣泛的使用者案例。無論何時,當一個問題涉及到那些想要有針對性的選擇需要接收訊息的 多消費者/多應用(multiple consumers/applications) 的時候,主題交換機都可以被列入考慮範圍。

使用案例:

  • 分發有關於特定地理位置的資料,例如銷售點
  • 由多個工作者(workers)完成的後臺任務,每個工作者負責處理某些特定的任務
  • 股票價格更新(以及其他型別的金融資料更新)
  • 涉及到分類或者標籤的新聞更新(例如,針對特定的運動專案或者隊伍)
  • 雲端的不同種類服務的協調
  • 分散式架構/基於系統的軟體封裝,其中每個構建者僅能處理一個特定的架構或者系統。

頭交換機

有時訊息的路由操作會涉及到多個屬性,此時使用訊息頭就比用路由鍵更容易表達,頭交換機(headers exchange)就是為此而生的。頭交換機使用多個訊息屬性來代替路由鍵建立路由規則。通過判斷訊息頭的值能否與指定的繫結相匹配來確立路由規則。

我們可以繫結一個佇列到頭交換機上,並給他們之間的繫結使用多個用於匹配的頭(header)。這個案例中,訊息代理得從應用開發者那兒取到更多一段資訊,換句話說,它需要考慮某條訊息(message)是需要部分匹配還是全部匹配。上邊說的“更多一段訊息”就是"x-match"引數。當"x-match"設定為“any”時,訊息頭的任意一個值被匹配就可以滿足條件,而當"x-match"設定為“all”的時候,就需要訊息頭的所有值都匹配成功。

頭交換機可以視為直連交換機的另一種表現形式。頭交換機能夠像直連交換機一樣工作,不同之處在於頭交換機的路由規則是建立在頭屬性值之上,而不是路由鍵。路由鍵必須是一個字串,而頭屬性值則沒有這個約束,它們甚至可以是整數或者雜湊值(字典)等。

佇列

AMQP中的佇列(queue)跟其他訊息佇列或任務佇列中的佇列是很相似的:它們儲存著即將被應用消費掉的訊息。佇列跟交換機共享某些屬性,但是佇列也有一些另外的屬性。

  • Name
  • Durable(訊息代理重啟後,佇列依舊存在)
  • Exclusive(只被一個連線(connection)使用,而且當連線關閉後佇列即被刪除)
  • Auto-delete(當最後一個消費者退訂後即被刪除)
  • Arguments(一些訊息代理用他來完成類似與TTL的某些額外功能)

佇列在宣告(declare)後才能被使用。如果一個佇列尚不存在,宣告一個佇列會建立它。如果宣告的佇列已經存在,並且屬性完全相同,那麼此次宣告不會對原有佇列產生任何影響。如果宣告中的屬性與已存在佇列的屬性有差異,那麼一個錯誤程式碼為406的通道級異常就會被丟擲。

佇列名稱

佇列的名字可以由應用(application)來取,也可以讓訊息代理(broker)直接生成一個。佇列的名字可以是最多255位元組的一個utf-8字串。若希望AMQP訊息代理生成佇列名,需要給佇列的name引數賦值一個空字串:在同一個通道(channel)的後續的方法(method)中,我們可以使用空字串來表示之前生成的佇列名稱。之所以之後的方法可以獲取正確的佇列名是因為通道可以默默地記住訊息代理最後一次生成的佇列名稱。

以"amq."開始的佇列名稱被預留做訊息代理內部使用。如果試圖在佇列宣告時打破這一規則的話,一個通道級的403 (ACCESS_REFUSED)錯誤會被丟擲。

佇列持久化

持久化佇列(Durable queues)會被儲存在磁碟上,當訊息代理(broker)重啟的時候,它依舊存在。沒有被持久化的佇列稱作暫存佇列(Transient queues)。並不是所有的場景和案例都需要將佇列持久化。

持久化的佇列並不會使得路由到它的訊息也具有永續性。倘若訊息代理掛掉了,重新啟動,那麼在重啟的過程中持久化佇列會被重新宣告,無論怎樣,只有經過持久化的訊息才能被重新恢復。

繫結

繫結(Binding)是交換機(exchange)將訊息(message)路由給佇列(queue)所需遵循的規則。如果要指示交換機“E”將訊息路由給佇列“Q”,那麼“Q”就需要與“E”進行繫結。繫結操作需要定義一個可選的路由鍵(routing key)屬性給某些型別的交換機。路由鍵的意義在於從傳送給交換機的眾多訊息中選擇出某些訊息,將其路由給繫結的佇列。

打個比方:

  • 佇列(queue)是我們想要去的位於紐約的目的地
  • 交換機(exchange)是JFK機場
  • 繫結(binding)就是JFK機場到目的地的路線。能夠到達目的地的路線可以是一條或者多條

擁有了交換機這個中間層,很多由釋出者直接到佇列難以實現的路由方案能夠得以實現,並且避免了應用開發者的許多重複勞動。

如果AMQP的訊息無法路由到佇列(例如,傳送到的交換機沒有繫結佇列),訊息會被就地銷燬或者返還給釋出者。如何處理取決於釋出者設定的訊息屬性。

消費者

訊息如果只是儲存在佇列裡是沒有任何用處的。被應用消費掉,訊息的價值才能夠體現。在AMQP 0-9-1 模型中,有兩種途徑可以達到此目的:

  • 將訊息投遞給應用 ("push API")
  • 應用根據需要主動獲取訊息 ("pull API")

使用push API,應用(application)需要明確表示出它在某個特定佇列裡所感興趣的,想要消費的訊息。如是,我們可以說應用註冊了一個消費者,或者說訂閱了一個佇列。一個佇列可以註冊多個消費者,也可以註冊一個獨享的消費者(當獨享消費者存在時,其他消費者即被排除在外)。

每個消費者(訂閱者)都有一個叫做消費者標籤的識別符號。它可以被用來退訂訊息。消費者標籤實際上是一個字串。

訊息確認

消費者應用(Consumer applications) - 用來接受和處理訊息的應用 - 在處理訊息的時候偶爾會失敗或者有時會直接崩潰掉。而且網路原因也有可能引起各種問題。這就給我們出了個難題,AMQP代理在什麼時候刪除訊息才是正確的?AMQP 0-9-1 規範給我們兩種建議:

  • 當訊息代理(broker)將訊息傳送給應用後立即刪除。(使用AMQP方法:basic.deliver或basic.get-ok)
  • 待應用(application)傳送一個確認回執(acknowledgement)後再刪除訊息。(使用AMQP方法:basic.ack)

前者被稱作自動確認模式(automatic acknowledgement model),後者被稱作顯式確認模式(explicit acknowledgement model)。在顯式模式下,由消費者應用來選擇什麼時候傳送確認回執(acknowledgement)。應用可以在收到訊息後立即傳送,或將未處理的訊息儲存後傳送,或等到訊息被處理完畢後再發送確認回執(例如,成功獲取一個網頁內容並將其儲存之後)。

如果一個消費者在尚未傳送確認回執的情況下掛掉了,那AMQP代理會將訊息重新投遞給另一個消費者。如果當時沒有可用的消費者了,訊息代理會死等下一個註冊到此佇列的消費者,然後再次嘗試投遞。

拒絕訊息

當一個消費者接收到某條訊息後,處理過程有可能成功,有可能失敗。應用可以向訊息代理表明,本條訊息由於“拒絕訊息(Rejecting Messages)”的原因處理失敗了(或者未能在此時完成)。當拒絕某條訊息時,應用可以告訴訊息代理如何處理這條訊息——銷燬它或者重新放入佇列。當此佇列只有一個消費者時,請確認不要由於拒絕訊息並且選擇了重新放入佇列的行為而引起訊息在同一個消費者身上無限迴圈的情況發生。

Negative Acknowledgements

在AMQP中,basic.reject方法用來執行拒絕訊息的操作。但basic.reject有個限制:你不能使用它決絕多個帶有確認回執(acknowledgements)的訊息。但是如果你使用的是RabbitMQ,那麼你可以使用被稱作negative acknowledgements(也叫nacks)的AMQP 0-9-1擴充套件來解決這個問題。更多的資訊請參考幫助頁面

預取訊息

在多個消費者共享一個佇列的案例中,明確指定在收到下一個確認回執前每個消費者一次可以接受多少條訊息是非常有用的。這可以在試圖批量釋出訊息的時候起到簡單的負載均衡和提高訊息吞吐量的作用。For example, if a producing application sends messages every minute because of the nature of the work it is doing.(???例如,如果生產應用每分鐘才傳送一條訊息,這說明處理工作尚在執行。)

注意,RabbitMQ只支援通道級的預取計數,而不是連線級的或者基於大小的預取。

訊息屬性和有效載荷(訊息主體)

AMQP模型中的訊息(Message)物件是帶有屬性(Attributes)的。有些屬性及其常見,以至於AMQP 0-9-1 明確的定義了它們,並且應用開發者們無需費心思思考這些屬性名字所代表的具體含義。例如:

  • Content type(內容型別)
  • Content encoding(內容編碼)
  • Routing key(路由鍵)
  • Delivery mode (persistent or not)
    投遞模式(持久化 或 非持久化)
  • Message priority(訊息優先權)
  • Message publishing timestamp(訊息釋出的時間戳)
  • Expiration period(訊息有效期)
  • Publisher application id(釋出應用的ID)

有些屬性是被AMQP代理所使用的,但是大多數是開放給接收它們的應用直譯器用的。有些屬性是可選的也被稱作訊息頭(headers)。他們跟HTTP協議的X-Headers很相似。訊息屬性需要在訊息被髮布的時候定義。

AMQP的訊息除屬性外,也含有一個有效載荷 - Payload(訊息實際攜帶的資料),它被AMQP代理當作不透明的位元組陣列來對待。訊息代理不會檢查或者修改有效載荷。訊息可以只包含屬性而不攜帶有效載荷。它通常會使用類似JSON這種序列化的格式資料,為了節省,協議緩衝器和MessagePack將結構化資料序列化,以便以訊息的有效載荷的形式釋出。AMQP及其同行者們通常使用"content-type" 和 "content-encoding" 這兩個欄位來與訊息溝通進行有效載荷的辨識工作,但這僅僅是基於約定而已。

訊息能夠以持久化的方式釋出,AMQP代理會將此訊息儲存在磁碟上。如果伺服器重啟,系統會確認收到的持久化訊息未丟失。簡單地將訊息傳送給一個持久化的交換機或者路由給一個持久化的佇列,並不會使得此訊息具有持久化性質:它完全取決與訊息本身的持久模式(persistence mode)。將訊息以持久化方式釋出時,會對效能造成一定的影響(就像資料庫操作一樣,健壯性的存在必定造成一些效能犧牲)。

訊息確認

由於網路的不確定性和應用失敗的可能性,處理確認回執(acknowledgement)就變的十分重要。有時我們確認消費者收到訊息就可以了,有時確認回執意味著訊息已被驗證並且處理完畢,例如對某些資料已經驗證完畢並且進行了資料儲存或者索引操作。

這種情形很常見,所以 AMQP 0-9-1 內建了一個功能叫做 訊息確認(message acknowledgements),消費者用它來確認訊息已經被接收或者處理。如果一個應用崩潰掉(此時連線會斷掉,所以AMQP代理亦會得知),而且訊息的確認回執功能已經被開啟,但是訊息代理尚未獲得確認回執,那麼訊息會被從新放入佇列(並且在還有還有其他消費者存在於此佇列的前提下,立即投遞給另外一個消費者)。

協議內建的訊息確認功能將幫助開發者建立強大的軟體。

AMQP 0-9-1 方法

AMQP 0-9-1由許多方法(methods)構成。方法即是操作,這跟面向物件程式設計中的方法沒半毛錢關係。AMQP的方法被分組在類(class)中。這裡的類僅僅是對AMQP方法的邏輯分組而已。在AMQP 0-9-1參考 中有對AMQP方法的詳細介紹。

讓我們來看看交換機類,有一組方法被關聯到了交換機的操作上。這些方法如下所示:

  • exchange.declare
  • exchange.declare-ok
  • exchange.delete
  • exchange.delete-ok

(請注意,RabbitMQ網站參考中包含了特用於RabbitMQ的交換機類的擴充套件,這裡我們不對其進行討論)

以上的操作來自邏輯上的配對:exchange.declare 和 exchange.declare-ok,exchange.delete 和 exchange.delete-ok. 這些操作分為“請求 - requests”(由客戶端傳送)和“響應 - responses”(由代理髮送,用來回應之前提到的“請求”操作)。

如下的例子:客戶端要求訊息代理使用exchange.declare方法宣告一個新的交換機:

如上圖所示,exchange.declare方法攜帶了好幾個引數。這些引數可以允許客戶端指定交換機名稱、型別、是否持久化等等。

操作成功後,訊息代理使用exchange.declare-ok方法進行迴應:

exchange.declare-ok方法除了通道號之外沒有攜帶任何其他引數(通道-channel 會在本指南稍後章節進行介紹)。

AMQP佇列類的配對方法 - queue.declare方法 和 queue.declare-ok有著與其他配對方法非常相似的一系列事件:


不是所有的AMQP方法都有與其配對的“另一半”。許多(basic.publish是最被廣泛使用的)都沒有相對應的“響應”方法,另外一些(如basic.get)有著一種以上與之對應的“響應”方法。

連線

AMQP連線通常是長連線。AMQP是一個使用TCP提供可靠投遞的應用層協議。AMQP使用認證機制並且提供TLS(SSL)保護。當一個應用不再需要連線到AMQP代理的時候,需要優雅的釋放掉AMQP連線,而不是直接將TCP連線關閉。

通道

有些應用需要與AMQP代理建立多個連線。無論怎樣,同時開啟多個TCP連線都是不合適的,因為這樣做會消耗掉過多的系統資源並且使得防火牆的配置更加困難。AMQP 0-9-1提供了通道(channels)來處理多連線,可以把通道理解成共享一個TCP連線的多個輕量化連線。

在涉及多執行緒/程序的應用中,為每個執行緒/程序開啟一個通道(channel)是很常見的,並且這些通道不能被執行緒/程序共享。

一個特定通道上的通訊與其他通道上的通訊是完全隔離的,因此每個AMQP方法都需要攜帶一個通道號,這樣客戶端就可以指定此方法是為哪個通道準備的。

虛擬主機

為了在一個單獨的代理上實現多個隔離的環境(使用者、使用者組、交換機、佇列 等),AMQP提供了一個虛擬主機(virtual hosts - vhosts)的概念。這跟Web servers虛擬主機概念非常相似,這為AMQP實體提供了完全隔離的環境。當連線被建立的時候,AMQP客戶端來指定使用哪個虛擬主機。

AMQP是可擴充套件的

AMQP 0-9-1 擁有多個擴充套件點:

  • 定製化交換機型別 可以讓開發者們實現一些開箱即用的交換機型別尚未很好覆蓋的路由方案。例如 geodata-based routing。
  • 交換機和佇列的宣告中可以包含一些訊息代理能夠用到的額外屬性。例如RabbitMQ中的per-queue message TTL即是使用該方式實現。
  • 特定訊息代理的協議擴充套件。例如RabbitMQ所實現的擴充套件。
  • 新的 AMQP 0-9-1 方法類可被引入。
  • 訊息代理可以被其他的外掛擴充套件,例如RabbitMQ的管理前端 和 已經被外掛化的HTTP API。

這些特性使得AMQP 0-9-1模型更加靈活,並且能夠適用於解決更加寬泛的問題。

AMQP 0-9-1 客戶端生態系統

AMQP 0-9-1 擁有眾多的適用於各種流行語言和框架的客戶端。其中一部分嚴格遵循AMQP規範,提供AMQP方法的實現。另一部分提供了額外的技術,方便使用的方法和抽象。有些客戶端是非同步的(非阻塞的),有些是同步的(阻塞的),有些將這兩者同時實現。有些客戶端支援“供應商的特定擴充套件”(例如RabbitMQ的特定擴充套件)。

因為AMQP的主要目標之一就是實現互動性,所以對於開發者來講,瞭解協議的操作方法而不是隻停留在弄懂特定客戶端的庫就顯得十分重要。這樣一來,開發者使用不同型別的庫與協議進行溝通時就會容易的多。