Kafka訊息分發、主題分割槽與消費組的概念

banq發表於2018-03-14
本文主要從Kafka與傳統JMS訊息系統的對比中挖掘Kafka在訊息分發和主題分割槽上的獨特特點,Kafka透過主題topic、分割槽和消費組這三個概念靈活適應各種訊息場合,業務設計關鍵是如何用好這三個概念,當然前提是理解這三個概念內在機制和邏輯性。

以下是英文原文混譯:

當從其他訊息傳遞系統轉向Apache Kafka時,需要首先跨越一個概念性的障礙,那就是 - 訊息傳送到這個主題Topic上,這個主題到底是什麼東東,它內部的訊息分發如何工作的?

與普通訊息系統不同的是,卡夫卡只有一種訊息型別 - 那就是“主題topic”(這裡將其稱為KTopic,以便將其與JMS主題topic區分開來)。kTopic的基礎底層是一個被稱為日誌的持久資料結構(可以認為它像一個陣列),其中有指標(通常是一個數字偏移量)是代表地址的一個索引。消費者與消費者組訂閱k主題Topic,消費者組使用group ID表示; 您可以認為group ID是一種命名指標。

如果你不將group ID告訴訊息中介軟體伺服器broker,它就會將消費者讀取指標指向日誌的開頭(這樣導致broker將其接受到的所有訊息傳送給了所有的消費者),或者結束(所有消費者從下一條訊息開始獲取所有訊息)。消費一條訊息意味著將指標移動到日誌中的下一個位置。指標也可以向後移動以消費先前已消費的訊息,這與JMS等傳統訊息系統的消費後訊息會被刪除的機制非常不同。

這種不同,意味著消費者可以透過程式碼控制自己的日誌指標(偏移量)來定位自己獨特的起始位置。而在普通JMS中,消費者是無法自己控制自己的讀取訊息的位置的,因為這種狀態控制被訊息中介軟體伺服器內部自己控制了。

(Kafka提供消費組的概念,也就是對消費者進行分組,每個消費組都有自己單獨的日誌指標,單個消費組內部不同消費者會不斷偏移這個指標,導致同一個消費組內多個消費者不會獲得相同的訊息,但是多個消費組會獲得相同的訊息。)

能夠清晰解釋Kafka的kTopic訊息分發機制的最佳方式是將其與常規(非Kafka)訊息中介軟體進行對比。讓我們看看傳統的JMS世界,它有兩種訊息模型:

1. 佇列 - 先進先出
2. 主題 - 釋出/訂閱

在佇列模型中,訊息按照它們傳送的相同順序進行分發; 如果有不止一個消費者,這些消費者將以round-robin挨個輪詢方式接受訊息,確保公平獲得訊息。對於這種情況有一個問題是,在這種預設情況下,無法保證訊息將按順序處理,因為訊息是輪詢傳送給消費者的,順序是不確定的(某些消費者可能比其他消費者執行速度慢)。您可以使用諸如JMS 訊息組之類的功能來解決這個問題,其中相關訊息組被分配給消費者,並且這些組內的訊息被髮送給分配給該組的消費者。

這種佇列模型在卡夫卡中是透過使用kTopic 和消費組(主題topic+組group)來實現的。在這種情況下,如果一個消費組裡只有一個消費者,那麼這個消費者會收到所有訊息。但是當消費者裡有兩個消費者時會發生什麼?在這種情況下,最新的加入消費者從加入點接收所有未消費的訊息; 原來繁忙的消費者被暫停。這有點像JMS中的獨佔消費者功能,最後一位消費者後入為主的霸佔,排斥之前先加入的其他消費者;這 與JMS的公平分享特點形成鮮明對比。

卡夫卡的基本設計目標之一是快速生產和消費。這導致了一些有趣的設計權衡。最主要的是使用日誌作為基本資料構造 - 它是一個非常簡單的資料結構,可以非常快速地讀取和寫入磁碟。這是因為磁碟的順序訪問非常高效。

對於普通JMS,按照round-robin挨個輪詢方式從日記中分發訊息,一般是在JMS訊息伺服器上配置專門的排程執行緒以協調訊息挨個公平地傳遞給消費者。這會讓越來越多的消費者變得越來越慢(儘管大多數時候通常都不會看到消費者這種變慢情況)。卡夫卡則不同,它定位在與消費者的數量成線性關係,這意味著您就減少了協調工作(同時也避免單點風險,因為協調器本身就是一個單點瓶頸)。

但是,請考慮以下幾點:

1. 協調是併發的基本屬性。
2. 水平縮放應透過使用並行性策略來實現的。
3. 併發性反而會阻礙並行性。(併發是基於單點的,類似千軍萬馬過獨木橋,而並行則沒有單點)

所以,在佇列場景中獲得效能水平縮放的唯一方法是有效地並行同時使用多個日誌。

卡夫卡透過一個分割槽一個日誌的想法實現了這一點。在Kafka伺服器上,您可以定義每個kTopic有多少個分割槽。一個分割槽對應於一個日誌。如果您想在N個消費者中公平地分發訊息,則需要N個分割槽。

(Kafka每個分割槽代表一個日誌,實現一個主題下多分割槽,類似資料庫的分割槽一樣,與Redis的叢集分割槽原理一樣,比如100個數字,0-10在1分割槽,10-20在2分割槽,你也可以將偶數分配到一個分割槽 奇數分配到另外一個分割槽,也可以根據業務key進行特定分割槽,某個地區對應一個分割槽等等)

那麼訊息如何跨分割槽釋出?當訊息傳送到kTopic時,它們是透過Producer類的方法傳送:

send(ProducerRecord<K,V> record): Future<RecordMetadata>;

ProducerRecord包含一個鍵(K)和一個值(V)。值是訊息的有效負荷(也就是通常業務資料,與JMS中不同,不允許透過訊息傳送另外的訊息後設資料)。鍵K是用來實現跨多個分割槽進行訊息分片的。

客戶端有責任決定向哪個分割槽傳送訊息,這是透過實現介面Partitioner來對Producer進行配置的。預設對訊息進行跨區分片的演算法是key.hashCode() % numberOfPartitions(key的雜湊值除分割槽個數)。由於雜湊雜湊的衝突,這種預設情況不會為一組key提供公平的均等的分片機會,這就意味著如果您被分割槽到較少數量的分割槽(對於相同數量的並行使用者),那麼這些分割槽可能獲得比分割槽獲得更多的訊息。甚至在2個分割槽的情況下,其中一個可能根本得不到任何訊息 - 導致消費者飢餓。你能實現你自己的Partitioner子類提供更符合你自己業務特定的分割槽功能來平衡這一點。

重要的是需要注意:你的Partitioner子類實現必須始終將相關訊息放入同一個分割槽,否則將導致訊息失序,而相關訊息的順序性對於我們業務是很重要的,比如訂單訊息應該在支付訊息之前到達,如果沒有這種順序,後續工序如果首先接受到支付訊息,它無從知曉這是哪個訂單的支付,增加消費者手工程式碼工作量。

分割槽被公平分配給消費者,並在新增消費者或刪除消費者時再重新將消費者分配給多個分割槽。假設2個分割槽:

1. 如果您有1個消費者,它將從這兩個分割槽接受訊息。
2. 如果你有2個消費者,每個將單獨從一個分割槽接受訊息。
3. 如果你有3個,第三個將會停止,不會收到任何訊息。

在JMS主題中,訊息分發給所有可能感興趣的消費者。在kTopics中,這是透過為每個消費者提供他們自己的組group ID來實現的,(也就是說,如果你有N個分割槽,那麼就要設計一個消費組內有N個消費者,這樣每個消費者消費一個分割槽,這分割槽才真正等同於傳統訊息系統的佇列模型)。每個消費者的分發邏輯將與上面的佇列場景完全相同。實際上,這意味著kTopics的行為與ActiveMQ的虛擬目標或JMS 2.0持久主題類似,但不同於純粹的JMS主題 - 因為後者不會將訊息寫入磁碟,而kTopics則可以。

希望這能夠解釋清楚卡夫卡訊息分發的基本原理。這是一個非常有趣的訊息傳遞技術,但它的一些行為與傳統訊息傳遞系統從根本上發生了90度改變。

總結
在Apache Kafka中,消費者組概念可以實現兩種訊息模型:

1. 同一消費者組中多個消費者之間是“互相競爭排斥”關係,每個消費者從一個或多個分割槽(分割槽是“自動”分配給這個消費者的)接收訊息時,其他消費者(被分配到其他不同分割槽)則不會接收相同的訊息。透過這種方式,我們可以將消費者的數量擴充套件到分割槽數量(一個消費者只讀取一個分割槽);在這種情況下,加入消費組的新消費者將處於空閒狀態而不被分配給任何分割槽。

2.將消費者作為不同消費組中的一部分意味著提供“釋出/訂閱”模式,同一主題分割槽的訊息將傳送給不同消費者組中的所有消費者。這意味著在同一個消費組中,我們將擁有前面第一個規則,但是位於不同的消費組中多個消費者會收到相同的訊息。當不同的應用程式對同一個主題中的訊息感興趣時,這種釋出訂閱模式將非常有用,這些應用程式位於不同的消費組中,能夠接受到相同訊息,但是以不同的方式處理這些訊息。

為了更清晰理解Kafka的分割槽與消費組概念,可參看該文github專案

原文:

Message Distribution and Topic Partitioning in Kaf

相關文章