RabbitMQ 七戰 Kafka,差異立現!
作爲一個有豐富經驗的微服務系統架構師,經常有人問我,“應該選擇 RabbitMQ 還是 Kafka?”。基於某些原因, 許多開發者會把這兩種技術當做等價的來看待。的確,在一些案例場景下選擇 RabbitMQ 還是 Kafka 沒什麼差別,但是這兩種技術在底層實現方面是有許多差異的。
不同的場景需要不同的解決方案,選錯一個方案能夠嚴重的影響你對軟件的設計,開發和維護的能力。
這篇文章會先介紹 RabbitMQ 和 Apache Kafka 內部實現的相關概念。緊接着會主要介紹這兩種技術的主要不同點以及他們各自的優缺點,最後我們會說明一下怎樣選擇這兩種技術。
一、異步消息模式
異步消息可以作爲解耦消息的生產和處理的一種解決方案。提到消息系統,我們通常會想到兩種主要的消息模式——消息隊列和發佈 / 訂閱模式。
1、消息隊列
利用消息隊列可以解耦生產者和消費者。多個生產者可以向同一個消息隊列發送消息;但是,一個消息在被一個消息者處理的時候,這個消息在隊列上會被鎖住或者被移除並且其他消費者無法處理該消息。也就是說一個具體的消息只能由一個消費者消費。
消息隊列
需要額外注意的是,如果消費者處理一個消息失敗了,消息系統一般會把這個消息放回隊列,這樣其他消費者可以繼續處理。消息隊列除了提供解耦功能之外,它還能夠對生產者和消費者進行獨立的伸縮(scale),以及提供對錯誤處理的容錯能力。
2、發佈 / 訂閱
發佈 / 訂閱(pub/sub)模式中,單個消息可以被多個訂閱者併發的獲取和處理。
發佈 / 訂閱
例如,一個系統中產生的事件可以通過這種模式讓發佈者通知所有訂閱者。在許多隊列系統中常常用主題(topics)這個術語指代發佈 / 訂閱模式。在 RabbitMQ 中,主題就是發佈 / 訂閱模式的一種具體實現(更準確點說是交換器(exchange)的一種),但是在這篇文章中,我會把主題和發佈 / 訂閱當做等價來看待。
一般來說,訂閱有兩種類型:
1)臨時(ephemeral)訂閱,這種訂閱只有在消費者啓動並且運行的時候才存在。一旦消費者退出,相應的訂閱以及尚未處理的消息就會丟失。
2)持久(durable)訂閱,這種訂閱會一直存在,除非主動去刪除。消費者退出後,消息系統會繼續維護該訂閱,並且後續消息可以被繼續處理。
二、RabbitMQ
RabbitMQ 作爲消息中間件的一種實現,常常被當作一種服務總線來使用。RabbitMQ 原生就支持上面提到的兩種消息模式。其他一些流行的消息中間件的實現有 ActiveMQ,ZeroMQ,Azure Service Bus 以及 Amazon Simple Queue Service(SQS)。這些消息中間件的實現有許多共通的地方,這邊文章中提到的許多概念大部分都適用於這些中間件。
1、隊列
RabbitMQ 支持典型的開箱即用的消息隊列。開發者可以定義一個命名隊列,然後發佈者可以向這個命名隊列中發送消息。最後消費者可以通過這個命名隊列獲取待處理的消息。
2、消息交換器
RabbitMQ 使用消息交換器來實現發佈 / 訂閱模式。發佈者可以把消息發佈到消息交換器上而不用知道這些消息都有哪些訂閱者。
每一個訂閱了交換器的消費者都會創建一個隊列;然後消息交換器會把生產的消息放入隊列以供消費者消費。消息交換器也可以基於各種路由規則爲一些訂閱者過濾消息。
RabbitMQ 消息交換器
需要重點注意的是 RabbitMQ 支持臨時和持久兩種訂閱類型。消費者可以調用 RabbitMQ 的 API 來選擇他們想要的訂閱類型。
根據 RabbitMQ 的架構設計,我們也可以創建一種混合方法——訂閱者以組隊的方式然後在組內以競爭關係作爲消費者去處理某個具體隊列上的消息,這種由訂閱者構成的組我們稱爲消費者組。按照這種方式,我們實現了發佈 / 訂閱模式,同時也能夠很好的伸縮(scale-up)訂閱者去處理收到的消息。
發佈 / 訂閱與隊列的聯合使用
三、Apache Kafka
Apache Kafka 不是消息中間件的一種實現。相反,它只是一種分佈式流式系統。
不同於基於隊列和交換器的 RabbitMQ,Kafka 的存儲層是使用分區事務日誌來實現的。Kafka 也提供流式 API 用於實時的流處理以及連接器 API 用來更容易的和各種數據源集成;當然,這些已經超出了本篇文章的討論範圍。
雲廠商爲 Kafka 存儲層提供了可選的方案,比如 Azure Event Hubsy 以及 AWS Kinesis Data Streams 等。對於 Kafka 流式處理能力,還有一些特定的雲方案和開源方案,不過,話說回來,它們也超出了本篇的範圍。
1、主題
Kafka 沒有實現隊列這種東西。相應的,Kafka 按照類別存儲記錄集,並且把這種類別稱爲主題。
Kafka 爲每個主題維護一個消息分區日誌。每個分區都是由有序的不可變的記錄序列組成,並且消息都是連續的被追加在尾部。
當消息到達時,Kafka 就會把他們追加到分區尾部。默認情況下,Kafka 使用輪詢分區器(partitioner)把消息一致的分配到多個分區上。
Kafka 可以改變創建消息邏輯流的行爲。例如,在一個多租戶的應用中,我們可以根據每個消息中的租戶 ID 創建消息流。IoT 場景中,我們可以在常數級別下根據生產者的身份信息(identity)將其映射到一個具體的分區上。確保來自相同邏輯流上的消息映射到相同分區上,這就保證了消息能夠按照順序提供給消費者。
Kafka 生產者
消費者通過維護分區的偏移(或者說索引)來順序的讀出消息,然後消費消息。
單個消費者可以消費多個不同的主題,並且消費者的數量可以伸縮到可獲取的最大分區數量。
所以在創建主題的時候,我們要認真的考慮一下在創建的主題上預期的消息吞吐量。消費同一個主題的多個消費者構成的組稱爲消費者組。通過 Kafka 提供的 API 可以處理同一消費者組中多個消費者之間的分區平衡以及消費者當前分區偏移的存儲。
Kafka 消費者
2、Kafka 實現的消息模式
Kafka 的實現很好地契合發佈 / 訂閱模式。
生產者可以向一個具體的主題發送消息,然後多個消費者組可以消費相同的消息。每一個消費者組都可以獨立的伸縮去處理相應的負載。由於消費者維護自己的分區偏移,所以他們可以選擇持久訂閱或者臨時訂閱,持久訂閱在重啓之後不會丟失偏移而臨時訂閱在重啓之後會丟失偏移並且每次重啓之後都會從分區中最新的記錄開始讀取。
但是這種實現方案不能完全等價的當做典型的消息隊列模式看待。當然,我們可以創建一個主題,這個主題和擁有一個消費者的消費組進行關聯,這樣我們就模擬出了一個典型的消息隊列。不過這會有許多缺點,我們會在第二部分詳細討論。
值得特別注意的是,Kafka 是按照預先配置好的時間保留分區中的消息,而不是根據消費者是否消費了這些消息。這種保留機制可以讓消費者自由的重讀之前的消息。另外,開發者也可以利用 Kafka 的存儲層來實現諸如事件溯源和日誌審計功能。
儘管有時候 RabbitMQ 和 Kafka 可以當做等價來看,但是他們的實現是非常不同的。所以我們不能把他們當做同種類的工具來看待;一個是消息中間件,另一個是分佈式流式系統。
作爲解決方案架構師,我們要能夠認識到它們之間的差異並且儘可能的考慮在給定場景中使用哪種類型的解決方案。下面會指出這些差異並且提供什麼時候使用哪種方案的指導建議。
四、RabbitMQ 和 Kafka 的顯著差異
RabbitMQ 是一個消息代理,但是 Apache Kafka 是一個分佈式流式系統。好像從語義上就可以看出差異,但是它們內部的一些特性會影響到我們是否能夠很好的設計各種用例。
例如,Kafka 最適用於數據的流式處理,但是 RabbitMQ 對流式中的消息就很難保持它們的順序。
另一方面,RabbitMQ 內置重試邏輯和死信(dead-letter)交換器,但是 Kafka 只是把這些實現邏輯交給用戶來處理。
這部分主要強調在不同系統之間它們的主要差異。
1、消息順序
對於發送到隊列或者交換器上的消息,RabbitMQ 不保證它們的順序。儘管消費者按照順序處理生產者發來的消息看上去很符合邏輯,但是這有很大誤導性。
RabbitMQ 文檔中有關於消息順序保證的說明:
“發佈到一個通道(channel)上的消息,用一個交換器和一個隊列以及一個出口通道來傳遞,那麼最終會按照它們發送的順序接收到。”
——RabbitMQ 代理語義(Broker Semantics)
換話句話說,只要我們是單個消費者,那麼接收到的消息就是有序的。然而,一旦有多個消費者從同一個隊列中讀取消息,那麼消息的處理順序就沒法保證了。
由於消費者讀取消息之後可能會把消息放回(或者重傳)到隊列中(例如,處理失敗的情況),這樣就會導致消息的順序無法保證。
一旦一個消息被重新放回隊列,另一個消費者可以繼續處理它,即使這個消費者已經處理到了放回消息之後的消息。因此,消費者組處理消息是無序的,如下表所示:
使用 RabbitMQ 丟失消息順序的例子
當然,我們可以通過限制消費者的併發數等於 1 來保證 RabbitMQ 中的消息有序性。更準確點說,限制單個消費者中的線程數爲 1,因爲任何的並行消息處理都會導致無序問題。
不過,隨着系統規模增長,單線程消費者模式會嚴重影響消息處理能力。所以,我們不要輕易的選擇這種方案。
另一方面,對於 Kafka 來說,它在消息處理方面提供了可靠的順序保證。Kafka 能夠保證發送到相同主題分區的所有消息都能夠按照順序處理。
在前面說過,默認情況下,Kafka 會使用循環分區器(round-robin partitioner)把消息放到相應的分區上。不過,生產者可以給每個消息設置分區鍵(key)來創建數據邏輯流(比如來自同一個設備的消息,或者屬於同一租戶的消息)。
所有來自相同流的消息都會被放到相同的分區中,這樣消費者組就可以按照順序處理它們。
但是,我們也應該注意到,在同一個消費者組中,每個分區都是由一個消費者的一個線程來處理。結果就是我們沒法伸縮(scale)單個分區的處理能力。
不過,在 Kafka 中,我們可以伸縮一個主題中的分區數量,這樣可以讓每個分區分擔更少的消息,然後增加更多的消費者來處理額外的分區。
獲勝者(Winner):
顯而易見,Kafka 是獲勝者,因爲它可以保證按順序處理消息。RabbitMQ 在這塊就相對比較弱。
2、消息路由
RabbitMQ 可以基於定義的訂閱者路由規則路由消息給一個消息交換器上的訂閱者。一個主題交換器可以通過一個叫做 routing_key 的特定頭來路由消息。
或者,一個頭部(headers)交換器可以基於任意的消息頭來路由消息。這兩種交換器都能夠有效地讓消費者設置他們感興趣的消息類型,因此可以給解決方案架構師提供很好的靈活性。
另一方面,Kafka 在處理消息之前是不允許消費者過濾一個主題中的消息。一個訂閱的消費者在沒有異常情況下會接受一個分區中的所有消息。
作爲一個開發者,你可能使用 Kafka 流式作業(job),它會從主題中讀取消息,然後過濾,最後再把過濾的消息推送到另一個消費者可以訂閱的主題。但是,這需要更多的工作量和維護,並且還涉及到更多的移動操作。
獲勝者:
在消息路由和過濾方面,RabbitMQ 提供了更好的支持。
3、消息時序(timing)
在測定發送到一個隊列的消息時間方面,RabbitMQ 提供了多種能力:
1)消息存活時間(TTL)
發送到 RabbitMQ 的每條消息都可以關聯一個 TTL 屬性。發佈者可以直接設置 TTL 或者根據隊列的策略來設置。
系統可以根據設置的 TTL 來限制消息的有效期。如果消費者在預期時間內沒有處理該消息,那麼這條消息會自動的從隊列上被移除(並且會被移到死信交換器上,同時在這之後的消息都會這樣處理)。
TTL 對於那些有時效性的命令特別有用,因爲一段時間內沒有處理的話,這些命令就沒有什麼意義了。
2)延遲 / 預定的消息
RabbitMQ 可以通過插件的方式來支持延遲或者預定的消息。當這個插件在消息交換器上啓用的時候,生產者可以發送消息到 RabbitMQ 上,然後這個生產者可以延遲 RabbitMQ 路由這個消息到消費者隊列的時間。
這個功能允許開發者調度將來(future)的命令,也就是在那之前不應該被處理的命令。例如,當生產者遇到限流規則時,我們可能會把這些特定的命令延遲到之後的一個時間執行。
Kafka 沒有提供這些功能。它在消息到達的時候就把它們寫入分區中,這樣消費者就可以立即獲取到消息去處理。
Kafka 也沒用爲消息提供 TTL 的機制,不過我們可以在應用層實現。
不過,我們必須要記住的一點是 Kafka 分區是一種追加模式的事務日誌。所以,它是不能處理消息時間(或者分區中的位置)。
獲勝者:
毫無疑問,RabbitMQ 是獲勝者,因爲這種實現天然的就限制 Kafka。
4、消息留存(retention)
當消費者成功消費消息之後,RabbitMQ 就會把對應的消息從存儲中刪除。這種行爲沒法修改。它幾乎是所有消息代理設計的必備部分。
相反,Kafka 會給每個主題配置超時時間,只要沒有達到超時時間的消息都會保留下來。在消息留存方面,Kafka 僅僅把它當做消息日誌來看待,並不關心消費者的消費狀態。
消費者可以不限次數的消費每條消息,並且他們可以操作分區偏移來 “及時” 往返的處理這些消息。Kafka 會週期的檢查分區中消息的留存時間,一旦消息超過設定保留的時長,就會被刪除。
Kafka 的性能不依賴於存儲大小。所以,理論上,它存儲消息幾乎不會影響性能(只要你的節點有足夠多的空間保存這些分區)。
獲勝者:
Kafka 設計之初就是保存消息的,但是 RabbitMQ 並不是。所以這塊沒有可比性,Kafka 是獲勝者。
5、容錯處理
當處理消息,隊列和事件時,開發者常常認爲消息處理總是成功的。畢竟,生產者把每條消息放入隊列或者主題後,即使消費者處理消息失敗了,它僅僅需要做的就是重新嘗試,直到成功爲止。
儘管表面上看這種方法是沒錯的,但是我們應該對這種處理方式多思考一下。首先我們應該承認,在某些場景下,消息處理會失敗。所以,即使在解決方案部分需要人爲干預的情況下,我們也要妥善地處理這些情況。
消息處理存在兩種可能的故障:
1)瞬時故障——故障產生是由於臨時問題導致,比如網絡連接,CPU 負載,或者服務崩潰。我們可以通過一遍又一遍的嘗試來減輕這種故障。
2)持久故障——故障產生是由於永久的問題導致的,並且這種問題不能通過額外的重試來解決。比如常見的原因有軟件 bug 或者無效的消息格式(例如,損壞(poison)的消息)。
作爲架構師和開發者,我們應該問問自己:“對於消息處理故障,我們應該重試多少次?每一次重試之間我們應該等多久?我們怎樣區分瞬時和持久故障?”
最重要的是:“所有重試都失敗後或者遇到一個持久的故障,我們要做什麼?”
當然,不同業務領域有不同的回答,消息系統一般會給我們提供工具讓我們自己實現解決方案。
RabbitMQ 會給我們提供諸如交付重試和死信交換器(DLX)來處理消息處理故障。
DLX 的主要思路是根據合適的配置信息自動地把路由失敗的消息發送到 DLX,並且在交換器上根據規則來進一步的處理,比如異常重試,重試計數以及發送到 “人爲干預” 的隊列。
查看下面篇文章,它在 RabbitMQ 處理重試上提供了額外的可能模式視角。
鏈接:https://engineering.nanit.com/rabbitmq-retries-the-full-story-ca4cc6c5b493
在 RabbitMQ 中我們需要記住最重要的事情是當一個消費者正在處理或者重試某個消息時(即使是在把它返回隊列之前),其他消費者都可以併發的處理這個消息之後的其他消息。
當某個消費者在重試處理某條消息時,作爲一個整體的消息處理邏輯不會被阻塞。所以,一個消費者可以同步地去重試處理一條消息,不管花費多長時間都不會影響整個系統的運行。
消費者 1 持續的在重試處理消息 1,同時其他消費者可以繼續處理其他消息
和 RabbitMQ 相反,Kafka 沒有提供這種開箱即用的機制。在 Kafka 中,需要我們自己在應用層提供和實現消息重試機制。
另外,我們需要注意的是當一個消費者正在同步地處理一個特定的消息時,那麼同在這個分區上的其他消息是沒法被處理的。
由於消費者不能改變消息的順序,所以我們不能夠拒絕和重試一個特定的消息以及提交一個在這個消息之後的消息。你只要記住,分區僅僅是一個追加模式的日誌。
一個應用層解決方案可以把失敗的消息提交到一個 “重試主題”,並且從那個主題中處理重試;但是這樣的話我們就會丟失消息的順序。
我們可以在 Uber.com 上找到 Uber 工程師實現的一個例子。如果消息處理的時延不是關注點,那麼對錯誤有足夠監控的 Kafka 方案可能就足夠了。
如果消費者阻塞在重試一個消息上,那麼底部分區的消息就不會被處理
獲勝者:
RabbitMQ 是獲勝者,因爲它提供了一個解決這個問題的開箱即用的機制。
6、伸縮
有多個基準測試,用於檢查 RabbitMQ 和 Kafka 的性能。
儘管通用的基準測試對一些特定的情況會有限制,但是 Kafka 通常被認爲比 RabbitMQ 有更優越的性能。
Kafka 使用順序磁盤 I / O 來提高性能。
從 Kafka 使用分區的架構上看,它在橫向擴展上會優於 RabbitMQ,當然 RabbitMQ 在縱向擴展上會有更多的優勢。
Kafka 的大規模部署通常每秒可以處理數十萬條消息,甚至每秒百萬級別的消息。
過去,Pivotal 記錄了一個 Kafka 集羣每秒處理一百萬條消息的例子;但是,它是在一個有着 30 個節點集羣上做的,並且這些消息負載被優化分散到多個隊列和交換器上。
鏈接:https://content.pivotal.io/blog/rabbitmq-hits-one-million-messages-per-second-on-google-compute-engine
典型的 RabbitMQ 部署包含 3 到 7 個節點的集羣,並且這些集羣也不需要把負載分散到不同的隊列上。這些典型的集羣通常可以預期每秒處理幾萬條消息。
獲勝者:
儘管這兩個消息平臺都可以處理大規模負載,但是 Kafka 在伸縮方面更優並且能夠獲得比 RabbitMQ 更高的吞吐量,因此這局 Kafka 獲勝。
但是,值得注意的是大部分系統都還沒有達到這些極限!所以,除非你正在構建下一個非常受歡迎的百萬級用戶軟件系統,否則你不需要太關心伸縮性問題,畢竟這兩個消息平臺都可以工作的很好。
7、消費者複雜度
RabbitMQ 使用的是智能代理和傻瓜式消費者模式。消費者註冊到消費者隊列,然後 RabbitMQ 把傳進來的消息推送給消費者。RabbitMQ 也有拉取(pull)API;不過,一般很少被使用。
RabbitMQ 管理消息的分發以及隊列上消息的移除(也可能轉移到 DLX)。消費者不需要考慮這塊。
根據 RabbitMQ 結構的設計,當負載增加的時候,一個隊列上的消費者組可以有效的從僅僅一個消費者擴展到多個消費者,並且不需要對系統做任何的改變。
RabbitMQ 高效的伸縮
相反,Kafka 使用的是傻瓜式代理和智能消費者模式。消費者組中的消費者需要協調他們之間的主題分區租約(以便一個具體的分區只由消費者組中一個消費者監聽)。
消費者也需要去管理和存儲他們分區偏移索引。幸運的是 Kafka SDK 已經爲我們封裝了,所以我們不需要自己管理。
另外,當我們有一個低負載時,單個消費者需要處理並且並行的管理多個分區,這在消費者端會消耗更多的資源。
當然,隨着負載增加,我們只需要伸縮消費者組使其消費者的數量等於主題中分區的數量。這就需要我們配置 Kafka 增加額外的分區。
但是,隨着負載再次降低,我們不能移除我們之前增加的分區,這需要給消費者增加更多的工作量。儘管這樣,但是正如我們上面提到過,Kafka SDK 已經幫我們做了這個額外的工作。
Kafka 分區沒法移除,向下伸縮後消費者會做更多的工作
獲勝者:
根據設計,RabbitMQ 就是爲了傻瓜式消費者而構建的。所以這輪 RabbitMQ 獲勝。
五、如何選擇?
現在我們就如面對百萬美元問題一樣:“什麼時候使用 RabbitMQ 以及什麼時候使用 Kafka?” 概括上面的差異,我們不難得出下面的結論。
優先選擇 RabbitMQ 的條件:
-
高級靈活的路由規則;
-
消息時序控制(控制消息過期或者消息延遲);
-
高級的容錯處理能力,在消費者更有可能處理消息不成功的情景中(瞬時或者持久);
-
更簡單的消費者實現。
優先選擇 Kafka 的條件:
-
嚴格的消息順序;
-
延長消息留存時間,包括過去消息重放的可能;
-
傳統解決方案無法滿足的高伸縮能力。
大部分情況下這兩個消息平臺都可以滿足我們的要求。但是,它取決於我們的架構師,他們會選擇最合適的工具。當做決策的時候,我們需要考慮上面着重強調的功能性差異和非功能性限制。
這些限制如下:
-
當前開發者對這兩個消息平臺的瞭解;
-
託管雲解決方案的可用性(如果適用);
-
每種解決方案的運營成本;
-
適用於我們目標棧的 SDK 的可用性。
當開發複雜的軟件系統時,我們可能被誘導使用同一個消息平臺去實現所有必須的消息用例。但是,從我的經驗看,通常同時使用這兩個消息平臺能夠帶來更多的好處。
例如,在一個事件驅動的架構系統中,我們可以使用 RabbitMQ 在服務之間發送命令,並且使用 Kafka 實現業務事件通知。
原因是事件通知常常用於事件溯源,批量操作(ETL 風格),或者審計目的,因此 Kafka 的消息留存能力就顯得很有價值。
相反,命令一般需要在消費者端做額外處理,並且處理可以失敗,所以需要高級的容錯處理能力。
這裏,RabbitMQ 在功能上有很多閃光點。以後我可能會寫一篇詳細的文章來介紹,但是你必須記住 -- 你的里程(mileage)可能會變化,因爲適合性取決於你的特定需求。
六、總結思想
寫這篇文章是由於我觀察到許多開發者把這 RabbitMQ 和 Kafka 作爲等價來看待。我希望通過這篇文章的幫助能夠讓你獲得對這兩種技術實現的深刻理解以及它們之間的技術差異。
反過來通過它們之間的差異來影響這兩個平臺去給用例提供更好的服務。這兩個消息平臺都很棒,並且都能夠給多個用例提供很好的服務。
但是,作爲解決方案架構師,取決於我們對每一個用例需求的理解,以及優化,然後選擇最合適的解決方案。
原文地址
-
https://medium.com/better-programming/rabbitmq-vs-kafka-1ef22a041793
-
https://medium.com/better-programming/rabbitmq-vs-kafka-1779b5b70c41
譯者丨王歡,Golang 後端工程師,DockOne 社區譯者
來源 | 分佈式實驗室(ID:dockerone)
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/m75N5hEa3PWLwx7F0Mi9tQ