雖然在以往的項目開發過程中已經使用過RabbitMQ與Kafka,但還是不能準確并全面的總結出它們倆之間的差異。
在這之前很長一段時間一直都是把這兩種技術當做等價的來看待,突然想到如果是我在某種特定業務下來做選型的話,我要怎么選呢?萬一選錯了,對于軟件開發和后期的維護都會造成嚴重的影響。
所謂學而時習之,不亦說乎。溫故而知新,可以為師矣。所以通過官網和參考了一些博客,做了以下整理:
宏觀的差異,RabbitMQ與Kafka只是功能類似,并不是同類
RabbitMQ是消息中間件,Kafka是分布式流式系統。
RabbitMQ
被概括為“開源分布式消息代理”,用Erlang編寫,有助于在復雜的路由方案中有效地傳遞消息,可以通過服務器上啟用的插件進行擴展,高可用(隊列可以在集群中的機器上進行鏡像)
有隊列
作為消息中間件的一種實現,RabbitMQ支持典型的開箱即用的消息隊列。開發者定義一個命名隊列,然后發布者向這個隊列中發送消息。最后消費者通過這個命名隊列獲取待處理的消息。
RabbitMQ的發布/訂閱模式
RabbitMQ使用消息交換器(Exchange)來實現發布/訂閱模式。發布者可以把消息發布到消息交換器上而不用知道這些消息都有哪些訂閱者。每一個訂閱了交換器的消費者都會創建一個隊列;然后消息交換器會把生產的消息放入隊列以供消費者消費。消息交換器也可以基于各種路由規則為一些訂閱者過濾消息。
注意:RabbitMQ支持臨時和持久兩種訂閱類型。消費者可以調用RabbitMQ的API來選擇他們想要的訂閱類型
Apache Kafka
被描述為“分布式事件流平臺”,用Scala和Java編寫,促進了原始吞吐量,基于“分布式僅追加日志”的思想,該消息將消息寫入持久化到磁盤的日志末尾,客戶端可以選擇從該日志開始讀取的位置,高可用(Kafka群集可以在多個服務器之間分布和群集)
無隊列,按主題存儲
Kafka不是消息中間件的一種實現。它只是一種分布式流式系統,Kafka的存儲層是使用分區事務日志來實現的。
Kafka沒有實現隊列。Kafka按照類別存儲記錄集,并且把這種類別稱為主題(topic)。
Kafka為每個主題(topic)維護一個消息分區日志。每個分區都是由有序的不可變的記錄序列組成,并且消息都是連續的被追加在尾部。默認情況下,Kafka使用輪詢分區器(partitioner)把消息一致的分配到多個分區上。
消費者通過維護分區的偏移量(或者說索引)來順序的讀出消息,然后消費消息。單個消費者可以消費多個不同的主題,并且消費者的數量可以伸縮到可獲取的最大分區數量。
所以在創建主題的時候,需要考慮一下在創建的主題上預期的消息吞吐量。在消費同一個主題的多個消費者構成的組稱為消費者組中,通過Kafka提供的API可以處理同一消費者組中多個消費者之間的分區平衡以及消費者當前分區偏移的存儲。
Kafka的發布/訂閱模式
生產者向一個具體的主題發送消息,然后多個消費者組可以消費相同的消息。每一個消費者組都可以獨立的伸縮去處理相應的負載。由于消費者維護自己的分區偏移,所以他們可以選擇持久訂閱或者臨時訂閱,持久訂閱在重啟之后不會丟失偏移而臨時訂閱在重啟之后會丟失偏移并且每次重啟之后都會從分區中最新的記錄開始讀取。
但是這種實現方案不能完全等價的當做典型的消息隊列模式看待。當然,我們可以創建一個主題,這個主題和擁有一個消費者的消費組進行關聯,這樣我們就模擬出了一個典型的消息隊列。不過這會有許多缺點,例如:消費失敗不支持重試等,下面微觀的差異中會有說明 。
Kafka是按照預先配置好的時間保留分區中的消息,而不是根據消費者是否消費了這些消息。這種保留機制可以讓消費者自由的重讀之前的消息。另外,開發者也可以利用Kafka的存儲層來實現諸如事件溯源和日志審計功能。
微觀差異,類似功能的不同特點
Kafka支持消息有序性,RabbitMQ不保證消息的順序
RabbitMQ
RabbitMQ文檔中關于消息順序保證的說明:
“發到一個通道(channel)上的消息,用一個交換器和一個隊列以及一個出口通道來傳遞,那么最終會按照它們發送的順序接收到。”
在RabbitMQ中只要我們是單個消費者(并且通過限制消費者的并發數等于1,不過,隨著系統規模增長,單線程消費者模式會嚴重影響消息處理能力),那么接收到的消息就是有序的。然而,一旦有多個消費者從同一個隊列中讀取消息,那么消息的處理順序就沒法保證了。
由于消費者讀取消息之后可能會把消息放回(或者重傳)到隊列中(例如,處理失敗的情況),這樣就會導致消息的順序無法保證。一旦一個消息被重新放回隊列,另一個消費者可以繼續處理它,即使這個消費者已經處理到了放回消息之后的消息。因此,消費者組處理消息是無序的。
Kafka
Kafka在消息處理方面提供了可靠的順序保證。
Kafka能夠保證發送到相同主題分區的所有消息都能夠按照順序處理。
所有來自相同流的消息都會被放到相同的分區中,這樣消費者組就可以按照順序處理它們。在同一個消費者組中,每個分區都是由一個消費者的一個線程來處理。結果就是我們沒法伸縮(scale)單個分區的處理能力。
不過,在Kafka中,我們可以伸縮一個主題中的分區數量,這樣可以讓每個分區分擔更少的消息,然后增加更多的消費者來處理額外的分區。
在消息路由和過濾方面,RabbitMQ提供了更好的支持
RabbitMQ
RabbitMQ可以基于定義的訂閱者路由規則路由消息給一個消息交換器上的訂閱者。一個主題交換器可以通過routingKey的特定頭來路由消息。
或者,headers交換器可以基于任意的消息頭來路由消息。這兩種交換器都能夠有效地讓消費者設置他們想要消息類型,因此可以給使用者提供了很好的靈活性。
Kafka
Kafka在處理消息之前是不允許消費者過濾一個主題中的消息。一個訂閱的消費者在沒有異常情況下會接受一個分區中的所有消息。
作為一個開發者,你可能使用Kafka流式作業(job),它會從主題中讀取消息,然后過濾,最后再把過濾的消息推送到另一個消費者可以訂閱的主題。但是,這需要更多的工作量和維護,并且還涉及到更多的移動操作。
消息時序
分布式系統中,很多業務場景都需要考慮消息投遞的時序,例如:
(1)單聊消息投遞,保證發送方發送順序與接收方展現順序一致
(2)群聊消息投遞,保證所有接收方展現順序一致
(3)充值支付消息,保證同一個用戶發起的請求在服務端執行序列一致
RabbitMQ
在保證消息時序方面,RabbitMQ提供了多種能力:
1)消息存活時間(TTL)
發送到RabbitMQ的每條消息都可以關聯一個TTL屬性。發布者可以直接設置TTL或者根據隊列的策略來設置。
系統可以根據設置的TTL來限制消息的有效期。如果消費者在預期時間內沒有處理該消息,那么這條消息會自動的從隊列上被移除(并且會被移到死信交換器上,同時在這之后的消息都會這樣處理)。
TTL對于那些有時效性的命令特別有用,因為一段時間內沒有處理的話,這些命令就沒有什么意義了。
2)延遲/預定的消息
RabbitMQ可以通過插件的方式來支持延遲或者預定的消息。當這個插件在消息交換器上啟用的時候,生產者可以發送消息到RabbitMQ上,然后這個生產者可以延遲RabbitMQ路由這個消息到消費者隊列的時間。
這個功能允許開發者調度將來(future)的命令,也就是在那之前不應該被處理的命令。例如,當生產者遇到限流規則時,我們可能會把這些特定的命令延遲到之后的一個時間執行。
Kafka
Kafka沒有提供這些功能。它在消息到達的時候就把它們寫入分區中,這樣消費者就可以立即獲取到消息去處理。Kafka也沒有為消息提供TTL的機制,不過我們可以在應用層實現。
注意:Kafka分區是一種追加模式的事務日志。所以,它是不能處理消息時間(或者分區中的位置)。
Kafka支持消息留存,RabbitMQ不支持
RabbitMQ
當消費者成功消費消息之后,RabbitMQ就會把對應的消息從存儲中刪除,且這種設定沒法修改。
Kafka
相反,Kafka會給每個主題配置超時時間,只要沒有達到超時時間的消息都會保留下來。在消息留存方面,Kafka僅僅把它當做消息日志來看待,并不關心消費者的消費狀態。
消費者可以不限次數的消費每條消息,并且他們可以操作分區偏移來“及時”往返的處理這些消息。Kafka會周期的檢查分區中消息的留存時間,一旦消息超過設定保留的時長,就會被刪除。
Kafka的性能不依賴于存儲大小。所以,理論上,它存儲消息幾乎不會影響性能(只要你的節點有足夠多的空間保存這些分區)。
RabbitMQ的容錯處理優于Kafka
消息處理存在兩種可能的故障:
1) 瞬時故障
故障產生是由于臨時問題導致,比如網絡連接或者服務崩潰等。我們可以通過多次測試來嘗試減輕這種故障。
2) 持久故障
故障產生是由于永久的問題導致的,并且這種問題不能通過額外的重試來解決。比如常見的原因有軟件bug或者無效的消息格式。
RabbitMQ
RabbitMQ提供了諸如交付重試和死信交換器(DLX)來處理消息處理故障。
DLX的主要思路是根據合適的配置信息自動地把路由失敗的消息發送到DLX,并且在交換器上根據規則來進一步的處理,比如異常重試,重試計數以及發送到“人為干預”的隊列。
在RabbitMQ中當一個消費者正在處理或者重試某個消息時(即使是在把它返回隊列之前),其他消費者都可以并發的處理這個消息之后的其他消息。
當某個消費者在重試處理某條消息時,作為一個整體的消息處理邏輯不會被阻塞。所以,一個消費者可以同步地去重試處理一條消息,不管花費多長時間都不會影響整個系統的運行。
消費者1持續的在重試處理消息1,同時其他消費者可以繼續處理其他消息
Kafka
Kafka沒有提供這種機制。需要我們自己在應用層提供和實現消息重試機制。
注意:當一個消費者正在同步地處理一個特定的消息時,那么同在這個分區上的其他消息是沒法被處理的。
由于消費者不能改變消息的順序,所以我們不能夠拒絕和重試一個特定的消息以及提交一個在這個消息之后的消息。
一個應用層解決方案:可以把失敗的消息提交到一個“重試主題”,并且從那個主題中處理重試;但是這樣的話我們就會丟失消息的順序。
如果消費者阻塞在重試一個消息上,那么底部分區的消息就不會被處理
Kafka在伸縮方面更優并且能夠獲得比RabbitMQ更高的吞吐量
RabbitMQ
典型的RabbitMQ部署包含3到7個節點的集群,并且這些集群也不需要把負載分散到不同的隊列上。
這些典型的集群通常可以預期每秒處理幾萬條消息。
Kafka
Kafka使用順序磁盤I / O來提高性能。
Kafka的大規模部署通常每秒可以處理數十萬條消息,甚至每秒百萬級別的消息。
Pivotal公司記錄了一個Kafka集群每秒處理一百萬條消息的例子;但是,它是在一個有著30個節點集群上做的,并且這些消息負載被優化分散到多個隊列和交換器上。
注意: 大部分系統都還沒有達到這些極限(反正我目前從未參與開發過此類系統)!所以,除非你正在構建下一個非常受歡迎的百萬級用戶軟件系統,否則你不需要太關心伸縮性問題,畢竟這兩個消息平臺都可以工作的很好。
RabbitMQ的消費者復雜度低于Kafka
RabbitMQ
RabbitMQ使用的是智能代理和傻瓜式消費者模式。
消費者注冊到消費者隊列,然后RabbitMQ把傳進來的消息推送給消費者。RabbitMQ也有拉取(pull)API;不過,一般很少被使用。
RabbitMQ管理消息的分發以及隊列上消息的移除(也可能轉移到DLX)。消費者不需要考慮這塊。
根據RabbitMQ結構的設計,當負載增加的時候,一個隊列上的消費者組可以有效的從僅僅一個消費者擴展到多個消費者,并且不需要對系統做任何的改變。
Kafka
Kafka使用的是傻瓜式代理和智能消費者模式。
消費者組中的消費者需要協調他們之間的主題分區租約(以便一個具體的分區只由消費者組中一個消費者監聽)。
消費者也需要去管理和存儲他們分區偏移索引。不過Kafka SDK已經為我們封裝了,所以我們不需要自己管理。
另外,當我們有一個低負載時,單個消費者需要處理并且并行的管理多個分區,這在消費者端會消耗更多的資源。
隨著負載增加,我們只需要伸縮消費者組使其消費者的數量等于主題中分區的數量。這就需要我們配置Kafka增加額外的分區。
但是,隨著負載再次降低,我們不能移除我們之前增加的分區,這需要給消費者增加更多的工作量。不過Kafka SDK已經幫我們做了這個額外的工作。
Kafka分區沒法移除,向下伸縮后消費者會做更多的工作
結論
首先是在不考慮一些非功能性限制(如運營成本,開發人員對兩個平臺的了解等)的情況下:
優先選擇RabbitMQ的條件
- 高級靈活的路由規則。
- 消息時序控制(控制消息過期或者消息延遲)。
- 高級的容錯處理能力,在消費者更有可能處理消息不成功的情景中(瞬時或者持久)。
- 更簡單的消費者實現。
優先選擇Kafka的條件
- 嚴格的消息順序。
- 延長消息留存時間,包括過去消息重放的可能。
- 傳統解決方案無法滿足的高伸縮能力。