引言
按照Kafka默認的消費邏輯設定,一個分區只能被同一個消費組(ConsumerGroup)內的一個消費者消費。假設目前某消費組內只有一個消費者C0,訂閱了一個topic,這個topic包含7個分區,也就是說這個消費者C0訂閱了7個分區,參考下圖(1)。

此時消費組內又加入了一個新的消費者C1,按照既定的邏輯需要將原來消費者C0的部分分區分配給消費者C1消費,情形上圖(2),消費者C0和C1各自負責消費所分配到的分區,相互之間並無實質性的干擾。
接着消費組內又加入了一個新的消費者C2,如此消費者C0、C1和C2按照上圖(3)中的方式各自負責消費所分配到的分區。
如果消費者過多,出現了消費者的數量大於分區的數量的情況,就會有消費者分配不到任何分區。參考下圖,一共有8個消費者,7個分區,那么最后的消費者C7由於分配不到任何分區進而就無法消費任何消息。

上面各個示例中的整套邏輯是按照Kafka中默認的分區分配策略來實施的。Kafka提供了消費者客戶端參數partition.assignment.strategy用來設置消費者與訂閱主題之間的分區分配策略。默認情況下,此參數的值為:org.apache.kafka.clients.consumer.RangeAssignor,即采用RangeAssignor分配策略。除此之外,Kafka中還提供了另外兩種分配策略: RoundRobinAssignor和StickyAssignor。消費者客戶端參數partition.asssignment.strategy可以配置多個分配策略,彼此之間以逗號分隔。
RangeAssignor分配策略
RangeAssignor策略的原理是按照消費者總數和分區總數進行整除運算來獲得一個跨度,然后將分區按照跨度進行平均分配,以保證分區盡可能均勻地分配給所有的消費者。對於每一個topic,RangeAssignor策略會將消費組內所有訂閱這個topic的消費者按照名稱的字典序排序,然后為每個消費者划分固定的分區范圍,如果不夠平均分配,那么字典序靠前的消費者會被多分配一個分區。
假設n=分區數/消費者數量,m=分區數%消費者數量,那么前m個消費者每個分配n+1個分區,后面的(消費者數量-m)個消費者每個分配n個分區。
為了更加通俗的講解RangeAssignor策略,我們不妨再舉一些示例。假設消費組內有2個消費者C0和C1,都訂閱了主題t0和t1,並且每個主題都有4個分區,那么所訂閱的所有分區可以標識為:t0p0、t0p1、t0p2、t0p3、t1p0、t1p1、t1p2、t1p3。最終的分配結果為:
消費者C0:t0p0、t0p1、t1p0、t1p1 消費者C1:t0p2、t0p3、t1p2、t1p3
這樣分配的很均勻,那么此種分配策略能夠一直保持這種良好的特性呢?我們再來看下另外一種情況。假設上面例子中2個主題都只有3個分區,那么所訂閱的所有分區可以標識為:t0p0、t0p1、t0p2、t1p0、t1p1、t1p2。最終的分配結果為:
消費者C0:t0p0、t0p1、t1p0、t1p1 消費者C1:t0p2、t1p2
可以明顯的看到這樣的分配並不均勻,如果將類似的情形擴大,有可能會出現部分消費者過載的情況。對此我們再來看下另一種RoundRobinAssignor策略的分配效果如何。
RoundRobinAssignor分配策略
RoundRobinAssignor策略的原理是將消費組內所有消費者以及消費者所訂閱的所有topic的partition按照字典序排序,然后通過輪詢方式逐個將分區以此分配給每個消費者。RoundRobinAssignor策略對應的partition.assignment.strategy參數值為:org.apache.kafka.clients.consumer.RoundRobinAssignor。
如果同一個消費組內所有的消費者的訂閱信息都是相同的,那么RoundRobinAssignor策略的分區分配會是均勻的。舉例,假設消費組中有2個消費者C0和C1,都訂閱了主題t0和t1,並且每個主題都有3個分區,那么所訂閱的所有分區可以標識為:t0p0、t0p1、t0p2、t1p0、t1p1、t1p2。最終的分配結果為:
消費者C0:t0p0、t0p2、t1p1 消費者C1:t0p1、t1p0、t1p2
如果同一個消費組內的消費者所訂閱的信息是不相同的,那么在執行分區分配的時候就不是完全的輪詢分配,有可能會導致分區分配的不均勻。如果某個消費者沒有訂閱消費組內的某個topic,那么在分配分區的時候此消費者將分配不到這個topic的任何分區。
舉例,假設消費組內有3個消費者C0、C1和C2,它們共訂閱了3個主題:t0、t1、t2,這3個主題分別有1、2、3個分區,即整個消費組訂閱了t0p0、t1p0、t1p1、t2p0、t2p1、t2p2這6個分區。具體而言,消費者C0訂閱的是主題t0,消費者C1訂閱的是主題t0和t1,消費者C2訂閱的是主題t0、t1和t2,那么最終的分配結果為:
消費者C0:t0p0 消費者C1:t1p0 消費者C2:t1p1、t2p0、t2p1、t2p2
可以看到RoundRobinAssignor策略也不是十分完美,這樣分配其實並不是最優解,因為完全可以將分區t1p1分配給消費者C1。
StickyAssignor分配策略
我們再來看一下StickyAssignor策略,“sticky”這個單詞可以翻譯為“粘性的”,Kafka從0.11.x版本開始引入這種分配策略,它主要有兩個目的:
分區的分配要盡可能的均勻;
分區的分配盡可能的與上次分配的保持相同。
當兩者發生沖突時,第一個目標優先於第二個目標。鑒於這兩個目標,StickyAssignor策略的具體實現要比RangeAssignor和RoundRobinAssignor這兩種分配策略要復雜很多。我們舉例來看一下StickyAssignor策略的實際效果。
假設消費組內有3個消費者:C0、C1和C2,它們都訂閱了4個主題:t0、t1、t2、t3,並且每個主題有2個分區,也就是說整個消費組訂閱了t0p0、t0p1、t1p0、t1p1、t2p0、t2p1、t3p0、t3p1這8個分區。最終的分配結果如下:
消費者C0:t0p0、t1p1、t3p0 消費者C1:t0p1、t2p0、t3p1 消費者C2:t1p0、t2p1
這樣初看上去似乎與采用RoundRobinAssignor策略所分配的結果相同,但事實是否真的如此呢?再假設此時消費者C1脫離了消費組,那么消費組就會執行再平衡操作,進而消費分區會重新分配。如果采用RoundRobinAssignor策略,那么此時的分配結果如下:
消費者C0:t0p0、t1p0、t2p0、t3p0 消費者C2:t0p1、t1p1、t2p1、t3p1
如分配結果所示,RoundRobinAssignor策略會按照消費者C0和C2進行重新輪詢分配。而如果此時使用的是StickyAssignor策略,那么分配結果為:
消費者C0:t0p0、t1p1、t3p0、t2p0 消費者C2:t1p0、t2p1、t0p1、t3p1
可以看到分配結果中保留了上一次分配中對於消費者C0和C2的所有分配結果,並將原來消費者C1的“負擔”分配給了剩余的兩個消費者C0和C2,最終C0和C2的分配還保持了均衡。
如果發生分區重分配,那么對於同一個分區而言有可能之前的消費者和新指派的消費者不是同一個,對於之前消費者進行到一半的處理還要在新指派的消費者中再次復現一遍,這顯然很浪費系統資源。StickyAssignor策略如同其名稱中的“sticky”一樣,讓分配策略具備一定的“粘性”,盡可能地讓前后兩次分配相同,進而減少系統資源的損耗以及其它異常情況的發生。
到目前為止所分析的都是消費者的訂閱信息都是相同的情況,我們來看一下訂閱信息不同的情況下的處理。
舉例,同樣消費組內有3個消費者:C0、C1和C2,集群中有3個主題:t0、t1和t2,這3個主題分別有1、2、3個分區,也就是說集群中有t0p0、t1p0、t1p1、t2p0、t2p1、t2p2這6個分區。消費者C0訂閱了主題t0,消費者C1訂閱了主題t0和t1,消費者C2訂閱了主題t0、t1和t2。
如果此時采用RoundRobinAssignor策略,那么最終的分配結果如下所示(和講述RoundRobinAssignor策略時的一樣,這樣不妨贅述一下):
【分配結果集1】
消費者C0:t0p0 消費者C1:t1p0 消費者C2:t1p1、t2p0、t2p1、t2p2
如果此時采用的是StickyAssignor策略,那么最終的分配結果為:
【分配結果集2】
消費者C0:t0p0 消費者C1:t1p0、t1p1 消費者C2:t2p0、t2p1、t2p2
可以看到這是一個最優解(消費者C0沒有訂閱主題t1和t2,所以不能分配主題t1和t2中的任何分區給它,對於消費者C1也可同理推斷)。
假如此時消費者C0脫離了消費組,那么RoundRobinAssignor策略的分配結果為:
消費者C1:t0p0、t1p1 消費者C2:t1p0、t2p0、t2p1、t2p2
可以看到RoundRobinAssignor策略保留了消費者C1和C2中原有的3個分區的分配:t2p0、t2p1和t2p2(針對結果集1)。而如果采用的是StickyAssignor策略,那么分配結果為:
消費者C1:t1p0、t1p1、t0p0 消費者C2:t2p0、t2p1、t2p2
可以看到StickyAssignor策略保留了消費者C1和C2中原有的5個分區的分配:t1p0、t1p1、t2p0、t2p1、t2p2。
從結果上看StickyAssignor策略比另外兩者分配策略而言顯得更加的優異,這個策略的代碼實現也是異常復雜,如果讀者沒有接觸過這種分配策略,不妨使用一下來嘗嘗鮮。
原文鏈接:https://blog.csdn.net/u013256816/article/details/81123625
