眾所周知,Apache Kafka是基於生產者和消費者模型作為開源的分散式釋出訂閱訊息系統(當然,目前Kafka定位於an open-source distributed event streaming platform),由Scala和Java編寫。
Kafka提供了類似於JMS的特性,但設計上又有很大區別,它不是JMS規範的實現,如Kafka允許多個消費者主動拉取資料,而在JMS中只有點對點模式消費者才會主動拉取資料。
Kafka producer在向Kafka叢集傳送訊息時,需要指定topic,Kafka根據topic對訊息進行歸類(邏輯劃分),而一個topic通常會有多個partition分割槽,落到磁碟上就是多個partition目錄。
Kafka consumer為了及時消費訊息,會以Consumer Group(消費組)的形式,啟動多個consumer消費訊息。不同的消費組在消費訊息時彼此互不影響,同一個消費組的consumer協調在一起消費訂閱的topic所有分割槽訊息。這就引申一個問題:消費組中的consumer是如何確定自己該消費哪些分割槽的資料的?
Kafka提供了多種分割槽策略如RoundRobin(輪詢)、Range(按範圍),可通過引數partition.assignment.strategy進行配置。
一般情況下,在topic和消費組不發生變化,Kafka會根據topic分割槽、消費組情況等確定分割槽策略,但是當發生以下情況時,會觸發Kafka的分割槽重分配:
1. Consumer Group中的consumer發生了新增或者減少
-
同一個Consumer Group新增consumer
- Consumer Group訂閱的topic分割槽發生變化如新增分割槽
2. Consumer Group訂閱的topic分割槽發生變化如新增分割槽
本文通過下面的場景,來分別闡述Kafka主要的分配策略RoundRobin和Range:
Range Strategy
Range策略是針對topic而言的,在進行分割槽分配時,為了儘可能保證所有consumer均勻的消費分割槽,會對同一個topic中的partition按照序號排序,並對consumer按照字典順序排序。
然後為每個consumer劃分固定的分割槽範圍,如果不夠平均分配,那麼排序靠前的消費者會被多分配分割槽。具體就是將partition的個數除於consumer執行緒數來決定每個consumer執行緒消費幾個分割槽。如果除不盡,那麼前面幾個消費者執行緒將會多分配分割槽。
通過下面公式更直觀:
假設n = 分割槽數 / 消費者數量,m = 分割槽數 % 消費者執行緒數量,那麼前m個消費者每個分配n+1個分割槽,後面的(消費者執行緒數量 - m)個消費者每個分配n個分割槽。
舉個例子:
一個消費組CG1中有C0和C1兩個consumer,消費Kafka中的主題t1。t1的分割槽數為10,並且C1的num.streams為1,C2的num.streams為2。
經過排序後,分割槽為:0, 1, 2, 3, 4, 5, 6, 7, 8, 9;CG1中消費者執行緒為C0-0、C1-0、C1-1。然後因為 10除3除不盡,那麼消費者執行緒C0-0將會多分配分割槽,所以分割槽分配之後結果如下:
C0-0 將消費0、1、2、3分割槽 C1-0 將消費4、5、6分割槽 C1-1 將消費7、8、9分割槽
當存在有2個Kafka topic(t1和t2),它們都有有10個partition,那麼最後分割槽結果為:
C0-0 將消費t1主題的0、1、2、3分割槽以及t2主題的0、1、2、3分割槽 C1-0 將消費t1主題的4、5、6分割槽以及t2主題的4、5、6分割槽 C2-1 將消費t1主題的7、8、9分割槽以及t2主題的7、8、9分割槽
如上場景,隨著topic的增多,那麼針對每個topic,消費者C0-0都將多消費1個分割槽,topic越多比如為N個,C0-0消費的分割槽會比其他消費者明顯多消費N個分割槽。
可以明顯的看到這樣的分配並不均勻,如果將類似的情形擴大,有可能會出現部分消費者過載的情況,這就是Range分割槽策略的一個很明顯的弊端。
RoundRobin Strategy
RoundRobin策略的工作原理:將所有topic的partition組成TopicAndPartition列表,然後對TopicAndPartition列表按照hashCode進行排序:
val allTopicPartitions = ctx.partitionsForTopic.flatMap { case(topic, partitions) => info("Consumer %s rebalancing the following partitions for topic %s: %s" .format(ctx.consumerId, topic, partitions)) partitions.map(partition => { TopicAndPartition(topic, partition) }) }.toSeq.sortWith((topicPartition1, topicPartition2) => { /* * Randomize the order by taking the hashcode to reduce the likelihood of all partitions of a given topic ending * up on one consumer (if it has a high enough stream count). */ topicPartition1.toString.hashCode < topicPartition2.toString.hashCode })
最後按照RoundRobin風格將分割槽分別分配給不同的消費者。
使用RoundRobin策略必須滿足以下條件:
1. 同一個Consumer Group裡面的所有consumer的num.streams必須相等
2.每個consumer訂閱的topic必須相同
假設消費組CG1中有C0和C1兩個consumer的num.streams都為2。按照hashCode排序完的topic-partition組依次為t1-5, t1-3, t1-0, t1-8, t1-2, t1-1, t1-4, t1-7, t1-6, t1-9,我們的消費者排序為C0-0, C0-1, C1-0, C1-1,最後分割槽分配的結果為:
C0-0將消費t1-5、t1-2、t1-6分割槽 C0-1將消費t1-3、t1-1、t1-9分割槽 C1-0將消費t1-0、t1-4分割槽 C1-1將消費t1-8、t1-7分割槽
多個主題的分割槽分配和單個主題類似,這裡就不在介紹了。
上面RoundRobin要求每個consumer訂閱的topic必須相同,當訂閱的topic不同時,那麼在執行分割槽分配的時候就不是完全的輪詢分配,有可能會導致分割槽分配的不均勻。比如,某個consumer沒有訂閱消費組內的某個topic,那麼在分配分割槽的時候,這個consumer將分配不到這個topic的分割槽。
除了上述的介紹的RoundRobin和Range分配策略,Kafka還有Sticky分配策略,它主要有兩個目的:
-
分割槽的分配要儘可能的均勻
-
分割槽的分配儘可能的與上次分配的保持相同
當兩者發生衝突時,第一個目標優先於第二個目標。鑑於這兩個目標,StickyAssignor策略的具體實現要比RangeAssignor和RoundRobinAssignor這兩種分配策略要複雜很多。
推薦文章:
Kafka中sequence IO、PageCache、SendFile的應用詳解
SparkStreaming和Kafka基於Direct Approach如何管理offset
關注微信公眾號:大資料學習與分享,獲取更對技術乾貨