Consumer Group
提及Consumer Group,最先想到的就是Group與Consumer Client的關聯關係:
- 1,Consumer Group用group.id(String)作爲全局唯一標識符
- 2,每個Group可以有零個、一個或多個Consumer Client
- 3,每個Group可以管理零個、一個或多個Topic
- 4,Group下每個Consumer Client可同時訂閱Topic的一個或多個Partition
- 5,Group下同一個Partition只能被一個Client訂閱,多Group下的Client訂閱不受影響
Consumer Group的作用主要有:管理Partition的Offset信息;管理Consumer Client與Partition的分配。正因爲所有Partition的Offset信息是由Group統一管理,所以如果一個Partition有多個Consumer,那麼每個Consumer在該Partition上的Offset很可能會不一致,這樣會導致在Rebalance後賦值處理的Client的消費起點發生混亂;與此同時,這種場景也不符合Kafka中Partition消息消費的一致性;因此在同一Group下一個Partition只能對應一個Consumer Client。
接下來將通過介紹Group的管理者Coordinator來了解Group是如何管理Offset;此外通過介紹Group的Rebalance機制瞭解Partition分配的原理,並介紹如何通過代碼實現Rebalance的監控。下圖是筆者基於自己的理解總結繪製的邏輯圖,有不對的地方還請指正:
Group Coordinator
Group Coordinator是一個服務,每個Broker在啓動的時候都會啓動一個該服務。Group Coordinator的作用是用來存儲Group的相關Meta信息,並將對應Partition的Offset信息記錄到Kafka內置Topic(__consumer_offsets)中。Kafka在0.9之前是基於Zookeeper來存儲Partition的Offset信息(consumers/{group}/offsets/{topic}/{partition}),因爲ZK並不適用於頻繁的寫操作,所以在0.9之後通過內置Topic的方式來記錄對應Partition的Offset。
每個Group都會選擇一個Coordinator來完成自己組內各Partition的Offset信息,選擇的規則如下:
- 1,計算Group對應在__consumer_offsets上的Partition
- 2,根據對應的Partition尋找該Partition的leader所對應的Broker,該Broker上的Group Coordinator即就是該Group的Coordinator
Partition計算規則
partition-Id(__consumer_offsets) = Math.abs(groupId.hashCode() % groupMetadataTopicPartitionCount)
其中groupMetadataTopicPartitionCount對應offsets.topic.num.partitions參數值,默認值是50個分區
查看指定Partition各Replica的分佈情況
bin/kafka-topics.sh --zookeeper <address:port> --topic __consumer_offsets --describe
Group Coordinator Generation
Group Coordinaror是Group Rebalance中不可或缺的一環,因爲Coordinator負責記錄了每次Rebalance後的Partition分配結果,因此Kafka爲Coordinator賦予了一個Generation的概念。Generation(谷歌翻譯爲“代”)可以通俗的理解爲版本的概念,Generation未開始時值爲-1,在第一次Rebalance後Group進入Stable狀態時值爲1,此後每發生一次Rebalance,Generation的Id會自增長加1。
網上有的人把Generation歸結爲Group的概念,說Group是有版本(代)的概念,個人覺得這是不太恰當的,因爲從源碼層面看,Generation是抽象類AbstractCoordinator的內部類;個人感覺Group更像是一個邏輯側面的概念,用來規範、管理一些消費端抽象抽來的一種約束手段。
Partition Assignment
Partition的分配策略和分配執行均是有Consumer Client完成的,而不是由Server(Group Coordinator)決定、執行的。因爲如果換做用Server端實現,則不僅會增加Broker的負擔,同時無法靈活的改變分配策略。
Assignment Strategy
Kafka目前支持RangeAssignor、RoundRobinAssignor兩種內置的分配策略,在0.11.x以後還內置了一種分配策略StickyAssignor;當然因爲Partition的分配策略是有Client控制的,所以Kafka支持用戶自定義分配策略。這裏詳細介紹一下前兩種分配策略
RangeAssignor
按照範圍進行劃分:對於每個被訂閱的Topic,儘可能將連續的Partition分給同一Consumer Client。假設一個Group下有m個Consumer同時訂閱一個Topic,該Topic有n個Partition,則具體的分配算法如下:
- 1,將Consumer Client按照命名排序
- 2,計算平均每個Consumer可以分到的Partition個數 => n / m
- 3,計算平均分配後剩餘的Partition個數 => n % m
- 4,如果n%m等於零,則代表所有客戶端可以一次平均分配到n/m個Partition;如果n%m大於零,則前n%m個Consumer分配到(n / m + 1)個Partition,剩下的Consumer(m - n % m)分配(n / m)個Partition
RangeAssignor分配策略是基於一個Topic而已的,如果同時訂閱多個Topic,則分別對每個Topic進行Range分配,接下來舉三個例子加以闡述:
Eg1:有兩個Consumer(分別爲C1,C2),同時訂閱兩個Topic(T1, T2),每個Topic都有4個Partition,則最終的分配結果爲:
C1:T1P0,T1P1,T2P0,T2P1
C2:T1P2,T1P3,T2P2,T2P3
Eg2:有兩個Consumer(分別爲C1,C2),同時訂閱兩個Topic(T1, T2),每個Topic都有3個Partition,則最終的分配結果爲:
C1:T1P0,T1P1,T2P0,T2P1
C2:T1P2,T2P2
Eg3:有兩個Consumer(分別爲C1,C2),同時訂閱兩個Topic(T1, T2),T1有3個Partition,T2有4個Partition,則最終的分配結果爲:
C1:T1P0,T1P1,T2P0,T2P1
C2:T1P2,T2P2,T2P3
RoundRobinAssignor
輪詢分配:將Group內所有Topic的Partition合併到一起,然後按順序依次將Partition分配給所有訂閱該Topic的Consumer(按照name排序);如果某個Topic只有一個Consumer訂閱,則該Consumer將獨自訂閱該Topic下的所有Partition。在上述三種場景下的分配結果分別爲
Eg1 => C1:T1P0,T1P2,T2P0, T2P2;C2:T1P1,T1P3,T2P1,T2P3
Eg2 => C1:T1P0,T1P2,T2P1;C2:T1P1,T2P0,T2P2
Eg3 => C1:T1P0,T1P2,T2P1,T2P3;C2:T1P1,T2P0,T2P2
Assignment Principle
- Step 1,GCR(GroupCoordinatorRequest)
- Step 2,JGR(JoinGroupRequest)
- Step 3,SGR(SyncGroupRequest)
首先根據GroupId選擇對應的Coordinator(Step1),然後Group內所有的Consumer Client向Coordinator發送JoinGroup請求,此時Coordinator會從所有Client中選擇一個作爲leader,其他的作爲follower;leader會根據客戶端所指定的分區分配策略執行分配任務,並將最終的分配結果發送到Coordinator(Step 2);最後所有的客戶端向Coordinator發送SyncGroup請求,用於獲取Partition的分區結果(Step 3)。
分配邏輯的源碼詳解可參考Kafka源碼深度解析-序列7 -Consumer -coordinator協議與heartbeat實現原理
Rebalance
Rebalance是一個分區-客戶端重分配協議。旨在特定條件下,基於給定的分配策略來爲Group下所有Consumer Client重新分配所要訂閱的Partition。Rebalance是Consumer Group中一個重要的特性,也爲Group提供了High Availability and Scalability。但同樣Rebalance也存在相應的弊端:在Rebalance期間,整個Group對外不可用。
- Rebalance 觸發條件
- Group中有新Consumer加入
- Group中已有的Consumer掛掉
- Coordinator掛了,集羣選出新Coordinator
- Topic新增Partition個數
- Consumer Client調用unsubscrible(),取消訂閱Topic
- Rebalance 過程
Rebalance的本質即就是Partition的分配;首先客戶端會向Coordinator發送JGR,等待leader發送Partition分配結果到Coordinator後,然後再向Coordinator發送SGR獲取分配結果。
Kafka通過Heartbeats(心跳)的方式實現Consumer Client與Coordinator之間的通信,用來相互告知對方的存在。如果Coordinator掛掉導致的Rebalance,則Kafka會重新選擇一個Coordinator,然後所有的Client會執行JGR、SGR;如果由於Client的變化導致Rebalance,則會通知有效Client進行JGR、SGR。
session.timeout.ms:Consumer Session過期時間;默認爲10000(10s);這個值的大小必須介於broker configuration中的group.min.session.timeout.ms 與 group.max.session.timeout.ms之間。
heartbeat.interval.ms: 心跳間隔;默認爲3000(3s);通常設置值低於session.timeout.ms的1/3
後續會對消費中遇到的所有時間戳做進一步的歸納整理
Rebalance Listener
因爲觸發Rebalance的可能性太多,並且在實際的工作中並不是所有的Rebalance都是有益的,所以可以在代碼層面實現對Rebalance的監控,從而根據真實的業務場景做出相應的對策。這裏貼出監控Demo:通過在客戶端維護Offset信息可以自定義控制消息的commit,儘可能保證Exactly Once語義,避免重複消費。
/**
* ConsumerRebalanceListener: 監聽客戶端Rebalance 包含兩個方法onPartitionsRevoked和onPartitionsAssigned
*
* onPartitionsRevoked: 在客戶端停止消費消息後、在Rebalance開始前調用可以在此時提交offset信息、保證在Rebalance後的consumer可以準確知曉Partition的消費起點
* onPartitionsAssigned:在Rebalance完成後調用
*
* @author yhyr
*/
public class ConsumerRebalanceListenerDemo {
private static Map<TopicPartition, OffsetAndMetadata> currentOffsets = new HashMap<>();
private static List<String> topics = Collections.singletonList("demoTopic");
private static KafkaConsumer<String, String> consumer;
static {
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "group_test");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
consumer = new KafkaConsumer<>(props);
}
private class CustomHandleRebalance implements ConsumerRebalanceListener {
@Override
public void onPartitionsRevoked(Collection<TopicPartition> collection) {
System.out.println("Before Rebalance, Assignment partitions is : " + collection.toString());
System.out.println("Before Rebalance, Each partition's lastest consumer offset : "
+ currentOffsets.toString());
}
@Override
public void onPartitionsAssigned(Collection<TopicPartition> collection) {
System.out.println("After Rebalance, Assignment partitions is : " + collection.toString());
System.out.println("After Rebalance, Each partition's lastest consumer offset : "
+ currentOffsets.toString());
}
}
private void consumer() {
// 通過自定義Rebalance監聽方式來訂閱Topic
consumer.subscribe(topics, new CustomHandleRebalance());
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
// deal with msg
System.out.println("Current Processing msg info : " + record.toString());
// increase offset
currentOffsets.put(new TopicPartition(record.topic(), record.partition()),
new OffsetAndMetadata(record.offset() + 1));
}
// submit offset by consumer.commitSync() or consumer.commitAsync() if you need; Default kafka auto commit
}
}
public static void main(String[] args) {
ConsumerRebalanceListenerDemo action = new ConsumerRebalanceListenerDemo();
action.consumer();
}
}
Group State
最後貼一個大牛整理的Group狀態機,其中的各種狀態轉換還未對其加以實踐;以後有機會結合源碼,再來做進一步的學習
圖片來Matt’s Blog