Kafka理論之Consumer Group & Coordinator

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

參考
Kafka權威指南
Consumer分區分配策略
Consumer Group介紹

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章