Kafka consumer group balance原理及源碼解讀(range/round robin/sticky)

 

目錄

序言

Range

算法

示例

Round Robin

算法

核心源碼

示例

Sticky

數據結構&算法

數據結構

算法

示例

核心方法

代碼步驟

平衡判斷


序言

kafka在0.11版本後提供了Range、Round Robin、Sticky三種consumer group partition分配策略,其中Range、Round Robin比較簡單,Sticky有些複雜,但Sticky的平衡效果最好。有點需要注意,consumer group partition分配策略是在consumer端完成分配計劃後發送給GroupCoordiantor,最後由GroupCoordinator傳播給其它consumer。這種設計有兩個優點,consumer完成分配計劃,減少了GroupCoordinator的壓力,增加了GroupCoordinator靈活性。由GroupCoordinator傳播給其它Consumer,避免了consumer之間互聯。(注意:本文說的消費均衡是參與消費的單個Consumer消費Partition數量均衡,不是指consumer group 中所有的Consumer進程之間的消費均衡

Range

Range策略是針對單個Topic設計的。如果Consumer Group只訂閱了單個Topic,那麼消費會很均衡。不論怎麼Rebalance,參與消費的consumer之間的partition數量之差最多爲1,理想情況下可以達到0,我們稱這個爲平衡分數(這個概念下面也會用到)。平衡分數越接近0平衡性越好,0是最完美的。如果Consumer Group訂閱了多個Topic,平衡分數比較大,訂閱的Topic越多,平衡分數越大,平衡效果越差,不建議Range策略使用在此場景下。

算法

//單個Topic的partition數量除以consumer數量,得每個consumer可得partition數量
int numPartitionsPerConsumer = numPartitionsForTopic / consumersForTopic.size();
//單個Topic的partition數量模consumer數量,得餘數
int consumersWithExtraPartition = numPartitionsForTopic % consumersForTopic.size();

List<TopicPartition> partitions = AbstractPartitionAssignor.partitions(topic, numPartitionsForTopic);
for (int i = 0, n = consumersForTopic.size(); i < n; i++) {
    //partition按區間分配給consumer
    int start = numPartitionsPerConsumer * i + Math.min(i, consumersWithExtraPartition);
    //加上餘數,加完爲止
    int length = numPartitionsPerConsumer + (i + 1 > consumersWithExtraPartition ? 0 : 1);
    assignment.get(consumersForTopic.get(i)).addAll(partitions.subList(start, start + length));
}

示例

1、consumer group 有三個consumer,分別爲C={C0、C1、C2},消費Topic T 有5個partition P={p0,p1,p2,p3,p4}

首初分配結果:

consumer數量不變與上面一致。如果C0掉線,分配結果如下:

2、consumer group 有三個consumer,分別爲C={C0、C1、C2},消費Topic t0有5個partition P={p0,p1,p2,p3,p4},T1有4個partition  P={p0,p1,p2,p3}

從上圖可以看出,consumer group 消費多Topic會出現不均衡,隨着Topic變多,均衡性越差。

Round Robin

Round Robin策略在設計中考慮了同一個Consumer Group消費多個Topic,因此消費多個Topic會表現的比較好的平衡性。同時也具有Range策略的優勢。但Consumer Group中各自consumer訂閱的Topic不同時Round Robin平衡性表現會比較差。

算法

1、基於字符順序構建有序consumer集合列表,基於取模特性,達到一個首尾相連有序環效果

      

2、基於Topic字符順序構建有序partition集合列表

3、遍歷有序partition集合,從有序環中取出consumer,並分配給partition

核心源碼

基於有序列表,達到首尾相連有序環的效果

@Override
public T next() {
    T next = list.get(i);
    //基於取模特性,下標介於[0,list.size-1]之間
    i = (i + 1) % list.size();
    return next;
}

示例

1、consumer group 有三個consumer,分別爲C={C0、C1、C2},消費Topic t0有5個partition P={p0,p1,p2,p3,p4},T1有4個partition  P={p0,p1,p2,p3}

2、consumer group 有三個consumer,分別爲C={C0、C1、C2}。3個Topic,分別爲T0的P={T0P0},T1的P={T0P0,T1P1},T2的P={T2P0,T2P1,P2P2}。 其中C0訂閱了T0、T1,C1訂閱了T0、T1,C2訂閱了T1、T2。即同一個consumer group下所有consumer訂閱的topic不同,這種情況下平衡性表現不理想。

Sticky

Sticky策略是我們今天重點要講的,Sticky不具有Range、Round Robin兩種策略的缺陷,在單Topic、多Topic、consumer group中Consumer訂閱的Topic不同等複雜情況下都有良好的平衡性。Sticky策略的源碼有些複雜,初看會覺的雜亂無序,而且註釋不多,但多看幾遍發現代碼邏輯挺清晰。

數據結構&算法

數據結構

Sticky策略沒有一個專門的數據結構,而是一個核心數據結構和多個輔助結構。採用Map<String,List<TopicPartition>>結構做爲核心數據結構,記錄分配情況,Key爲consumer,Value爲consumer分配的partition集合。如圖:

要做到上面的分配效果需要幾個輔助集合:

1、有序consumer TreeSet集合,以consumer當前分配的partition數量做升序

2、有序可分配的partition List集合,基於consumer順序,同一個consumer中以partition下標做升序

3、partition可被分配的consumer Map集合,key:partition,value:可分配的consumer集合

4、consumer可訂閱的partition Map集合,key:consumer,vlaue:可訂閱的partition集合

注意:第三個集合大於或者等於第四個集合,因爲第三個集合從Topic視角組裝集合

算法

算法邏輯上有幾個難點:

一、判斷平衡性

1、每個consumer被分配的partition數量相等或者差異爲1,那麼已經是很理想的平衡分數。

二、partition是否需要重新分配Consumer

爲方便說明記:

partition當前consumer訂閱的partition數量爲current.p.count

partition上個consumer訂閱的partition數量爲prev.p.count

partition可分配的consumer訂閱的partition數量爲potential.p.count

 1、檢查partition是否發生了generation衝突且current.p.count>prev.p.count+1,需要重新分配Consumer。

 2、檢查current.p.count>potential.p+1或者current.p.count+1<potential.p,需要重新分配Consumer

(備註:這裏需要注意下,在源碼裏面沒有current.p.count+1<potential.p判斷,這是源碼巧妙的地方,在performReassignments方法中只有達到平衡後纔會退出,在partitions爲基礎的死循環的過程中current和potential的視角是不斷變化的。例如:p0時,c1是current consumer,但在p1時,c1是potential consumer,所以源碼只做單方向判斷。)

三、粘性處理

粘性處理是Sticky策略最亮眼的地方,在保證最大可能的平衡的情況下,確保partition--->consumer的變化最少。在粘性處理準則與最大可能平衡平衡準則有衝突,以最大可能平衡優先。

邏輯:

1、構建分配策略的時候,優先保持原有的分配

2、記錄Partition最近一 次的移動軌跡,移動軌跡通過src---->dst表示。

假設T0P0最近一次的移動軌跡爲C1---->C2,本次計劃從C2---->C1(稱爲反轉),存在C2--->C1移動記錄,不允許這樣移動。從Partition的Topic移動軌跡中找一個移動軌跡一樣的Partition出來進行分配。

假設T0P0最近一次的移動軌跡爲C1---->C2,本次計劃從C2---->C3,反轉後變爲C3---->C1,不存C3---->C1移動記錄,可以移動。

示例

1、consumer group 有三個consumer,分別爲C={C0、C1、C2}。3個Topic,分別爲T0的P={T0P0},T1的P={T0P0,T1P1},T2的P={T2P0,T2P1,P2P2}。 其中C0訂閱了T0、T1,C1訂閱了T0、T1,C2訂閱了T1、T2。

如果C0被刪除,分配如下

核心方法

代碼步驟

1、構建當前的分配情況

     currentAssignment是基於上次訂閱情況構建,結構key:consumer, value:list<partition>,構建過程中有generation衝突的consumer,取generation最大的consumer

2、構建可能的分配組合

      從partition角度看,構建partition2AllPotentialConsumers

      從consumer角度看,構建consumer2AllPotentialPartitions

3、排序partitions

      對所有合法的partition進行排序,確保重分配階段partition在consumer之間移動最小

      排序原則:即訂閱partition量最多的cosumer涉及的partition排在前面,因爲這些partition最有可能需要重分配

      a. 重平衡且partition2AllPotentialConsumers中值均相等且consumer2AllPotentialPartitions中值均相等

      b. 

       //重分配且P與C呈倍數關係,需要進行sort partition操作

        //  操作CurrentAssignment

        //  1、過濾掉不存在的Partition集合

        //  2、產生Generation衝突的Partition集合與每個Consumer的Partitions做交集處理

        //     loop

        //     a. 存在交集:取交集中一個partition

        //     b. 不存在交集:取Consumer訂閱的一個partition

        //     end loop

        //  3、添加沒有被訂閱的partition

        //按照可訂閱Partition的consumer數量做升序排序,並以此順序轉換爲List集合

  4、平衡處理

     a. 分配未分配的partition

         遍歷sortedCurrentSubscriptions,按順序分配可分配的consumer,並對sortedCurrentSubscriptions重新排序 

             縮小重新分配的範圍

     b.過濾掉不需要重新分配的partition和consumer

         1、縮小到那些需要重新分配的partition

               尋找真正可以被重新分配的partition,一個partition可選擇的consumer小於2,說明是不可以重新分配

         2、縮小到那些需要重新分配的consumer

             a. consumer已經訂閱的p的數量小於可訂閱的p的數量,說明consumer是可重新分配的

             b. 一個partition可選擇的consumer大於等待2,說明consumer是可重新分配的

             c. 不符合a、b兩個條件的consumer爲不可重新分配的

     c.執行重分配

        1、遍歷有序partition

        2、檢查當前的分配策略是否均衡,檢查consumer訂閱的partition的數量

            a. 有序的consumer集合,比較first consumer與last consumer訂閱的partition數量,如果兩者相等或者差一,說明已經平衡

            b. 比較訂閱了同一個主題的兩個consumer所分配的partition數量之差

                (可訂閱但沒有訂閱的partition所對應的consumer訂閱的情況)

                 consumer可訂閱的partition集合L,consumer當前訂閱的partition集合M,

                 遍歷集合L,判斷L1是否在集合M中,若不在查看p當前的消費者c2,

                 判斷c1.count<c2.count(這裏的count指訂閱的所有topic),說明不均衡

        3、檢查partition是否需要重新分配

             a. 檢查partition是否屬於generation衝突且current consumer的partition數量大於prev consumer的partition.size+1,

                 說明需要重新分配

             b. 檢查partition可選擇的consumer訂閱的partition數量加1小於當前consumer訂閱的partition數量

        4、遍歷有序consumer(優先找訂閱少的consumer)尋找一個可訂閱當前partition的consumer並分配給它

        5、粘性分配

             a. 記錄每次分配或者partition遷移

             b.如果partition有遷移記錄,C1--->C2且C2----->C1,那麼改變分配的partition

        6、對比兩次分配的平衡效果。消費者訂閱partition數量的差異之和爲平衡分數,平衡分數越接近0越平衡。

平衡判斷

private boolean isBalanced(Map<String, List<TopicPartition>> currentAssignment,
                               TreeSet<String> sortedCurrentSubscriptions,
                               Map<String, List<TopicPartition>> allSubscriptions) {
        //有序consumer的首尾元素的partition數量差異
        int min = currentAssignment.get(sortedCurrentSubscriptions.first()).size();
        int max = currentAssignment.get(sortedCurrentSubscriptions.last()).size();
        //符合條件說明已經很平衡了
        if (min >= max - 1)
            return true;
        // create a mapping from partitions to the consumer assigned to them
        final Map<TopicPartition, String> allPartitions = new HashMap<>();
        Set<Entry<String, List<TopicPartition>>> assignments = currentAssignment.entrySet();
        for (Map.Entry<String, List<TopicPartition>> entry: assignments) {
            List<TopicPartition> topicPartitions = entry.getValue();
            for (TopicPartition topicPartition: topicPartitions) {
                if (allPartitions.containsKey(topicPartition))
                    log.error("{} is assigned to more than one consumer.", topicPartition);
                allPartitions.put(topicPartition, entry.getKey());
            }
        }

        // for each consumer that does not have all the topic partitions it can get make sure none of the topic partitions it
        // could but did not get cannot be moved to it (because that would break the balance)
        for (String consumer: sortedCurrentSubscriptions) {
            List<TopicPartition> consumerPartitions = currentAssignment.get(consumer);
            int consumerPartitionCount = consumerPartitions.size();

            // skip if this consumer already has all the topic partitions it can get
            // 滿足平衡要求
            if (consumerPartitionCount == allSubscriptions.get(consumer).size())
                continue;
            List<TopicPartition> potentialTopicPartitions = allSubscriptions.get(consumer);
            for (TopicPartition topicPartition: potentialTopicPartitions) {
                if (!currentAssignment.get(consumer).contains(topicPartition)) {
                    String otherConsumer = allPartitions.get(topicPartition);
                    int otherConsumerPartitionCount = currentAssignment.get(otherConsumer).size();
                    //關聯consumer之間比較partition數量,判斷平衡要求
                    if (consumerPartitionCount < otherConsumerPartitionCount) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

 執行分配

private boolean performReassignments(List<TopicPartition> reassignablePartitions,
                                         Map<String, List<TopicPartition>> currentAssignment,
                                         Map<TopicPartition, ConsumerGenerationPair> prevAssignment,
                                         TreeSet<String> sortedCurrentSubscriptions,
                                         Map<String, List<TopicPartition>> consumer2AllPotentialPartitions,
                                         Map<TopicPartition, List<String>> partition2AllPotentialConsumers,
                                         Map<TopicPartition, String> currentPartitionConsumer) {
        boolean reassignmentPerformed = false;
        boolean modified;

        // 執重複執行分配直到平衡爲止
        do {
            modified = false;
            Iterator<TopicPartition> partitionIterator = reassignablePartitions.iterator();
            //判斷是否平衡且可分配Partition不爲空
            while (partitionIterator.hasNext() && !isBalanced(currentAssignment, sortedCurrentSubscriptions, consumer2AllPotentialPartitions)) {
                TopicPartition partition = partitionIterator.next();
                if (partition2AllPotentialConsumers.get(partition).size() <= 1)
                    log.error("Expected more than one potential consumer for partition '{}'", partition);
                String consumer = currentPartitionConsumer.get(partition);
                if (consumer == null)
                    log.error("Expected partition '{}' to be assigned to a consumer", partition);

                //Generation衝突的Partition
                if (prevAssignment.containsKey(partition) &&
                        currentAssignment.get(consumer).size() > currentAssignment.get(prevAssignment.get(partition).consumer).size() + 1) {
                    reassignPartition(partition, currentAssignment, sortedCurrentSubscriptions, currentPartitionConsumer, prevAssignment.get(partition).consumer);
                    reassignmentPerformed = true;
                    modified = true;
                    continue;
                }

                //訂閱同一個Topic的consumer之間partition數量差異比較,判斷partition是否需要分配
                for (String otherConsumer: partition2AllPotentialConsumers.get(partition)) {
                    if (currentAssignment.get(consumer).size() > currentAssignment.get(otherConsumer).size() + 1) {
                        reassignPartition(partition, currentAssignment, sortedCurrentSubscriptions, currentPartitionConsumer, consumer2AllPotentialPartitions);
                        reassignmentPerformed = true;
                        modified = true;
                        break;
                    }
                }
            }
        } while (modified);

        return reassignmentPerformed;
    }

 

發佈了19 篇原創文章 · 獲贊 3 · 訪問量 3908
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章