Kafka分区分配策略

244 阅读21分钟

四种内置的分区策略总体上使用两种协议:

  1. RebalanceProtocol.EAGER
  2. RebalanceProtocol.COOPERATIVE 渐进式重平衡,于kafak2.4版本推出。

两种协议的有如下区别:

  • EAGER 重新平衡协议要求消费者在参与重新平衡事件之前始终撤销其拥有的所有分区。因此,它允许完全改组分配。

  • COOPERATIVE协议允许消费者在参与再平衡事件之前保留其当前拥有的分区。分配者不应该立即重新分配任何拥有的分区,而是可以指示消费者需要撤销分区,以便可以在下一次重新平衡事件中将被撤销的分区重新分配给其他消费者。COOPERATIVE协议将一次全局重平衡,改成每次小规模重平衡,直至最终收敛平衡的过程。 COOPERATIVE有效的改进来在此之前EAGER协议重平衡而触发的stop-the-world(STW)。

如下策略属于 RebalanceProtocol.EAGER 协议:

  1. RangeAssignor 范围分区分配策略
  2. RoundRobinAssignor 轮询分区策略
  3. StickyAssignor 粘性分区策略

如下策略属于 RebalanceProtocol.EAGER 协议:

  1. CooperativeStickyAssignor

下文将逐一介绍四种分区策略。

RangeAssignor

RangeAssignor分配策略存在一种特殊的机制,即 Rack Aware机制,当consumer和partition所在节点机器所在的机架(rack)号相同的时候,会优先分配此partition给这个consumer。如果未启用此机制的话,针对每个topic的partition,每个consumer至少分到 分区数量 / 消费者数量 个分区,同时剩余的分区数量 分区数量 % 消费者数量 按照 consumer的先后顺序(分配之前conusmer和partition都会被排序)依次分配。

partition和consumer排序

partition排序规则

partition按照编号从小到大排序即可。

consumer排序

consumer的排序稍微复杂一些,需要参考group.instance.idmember.id等。

  • 如果双方的group.instance.id都存在,则直接按字典序进行比较
  • 如果只有一方的group.instance.id存在,那么有group.instance.id的排在前面
  • 如果双方都不存在group.instance.id,那么按照各自的member.id基于字典序进行比较

consumer的member.id格式:${client.id}-UUID,默认的client.id的格式为:consumer-${group.id}-${编号}

未启用Rack Aware的分配流程

此流程针对于单个topic的所有partition。

现有如下partition和consumer:

partition: partition 0, partition 1, partition 2

consumer: consumer 0(consumer-test-group-1-UUID), consumer 1(consumer-test-group-2-UUID)

排序结果:

partition 0(编号:0), partition 1(编号:1), partition 2(编号:2)

consumer 0(consumer-test-group-1-UUID), consumer 1(consumer-test-group-2-UUID)

分配结果

总共有3个分区,两个消费者,那么每个消费者平均有 3(分区数) / 2(consumer数) = 1个分区,剩余1个分区(3%2),剩余的分区按照消费者的先后顺序分配,排在前面的优先分配 因此有如下分配结果:

kafka-Range分区分配结果.drawio.png

缺点

明显,排在前面的消费者实例会分配到更多的分区,某些情况下并不是很好的方案(比如前面的消费者当前的消费压力已经很大了,某个消息的消费时长过长,导致两次调用poll方法的间隔很长)。

启用Rack Aware的分配流程

启用条件

  • 所有consumer所在的rack和所有topic的partition副本所在的rack没有重叠,这种情况下没有启用Rack Aware的必要
  • 在consumer和partition的rack存在重叠的情况下,preferRackAwareLogic为true时,强制启用Rack Aware,不过此field仅在单测中使用,在正常使用场景下可以直接忽略
  • 在consumer和partition的rack存在重叠的情况下,每个partition的副本所在的rack完全一致,此种情况下无需启用Rack Aware。假设consumer 0 所在的rack为rack-0,既然和partition副本所在的rack有重叠,那么至少存在一个partition 0的某个副本也在rack-0,又因为每个partition的副本所在的rack完全一致,那么其他partition的副本中至少有一个也在rack-0上,即对于consumer 0来说,不管分配哪个分区,这个分区中总有一个副本的rack和consumer 0在一个rack上,也就没有所谓rack一致优先分配的必要了。所以,要启用Rack Aware,至少要有一个分区的副本所在rack和其他分区不一样。

为说明方便,对partition的副本做了简化处理,假设所有partition只有一个leader副本。

现有两个Topic,topic0-1,每个topic都各自有三个partition,partition0-2,以及两个consumer0-1,具体关系以及所在rack如下图所示。

kafka-rack-aware-3.drawio.png

具体流程

由于consumer 0和consumer 1 所在的rack同样存在partition,且两个topic的partition所在的rack不完全相同,因此满足启用Rack Aware机制的前提条件。

RangeAssignorRack Aware机制会将订阅了完全相同的topic的consumer放在一起处理,因此这里,consumer 0 和consumer 1会放在一起进行处理。进一步地,如果这些consumer订阅的topic中有相同数量的partition的,也会放到一起进行处理,因此这里会将topic 0 和topic 1 放到一起进行处理。

接下来,逐个分区开始分配。首先要从consumer中找到第一个其所在的rack和topic 0 和 topic 1 的对应编号的分区所在 rack 都相同 的consumer(实际上只要 consumer 和每个分区对应编号的分区的所有副本所在的rack有一个相同的即可,由于这里做了简化处理,每个分区的副本只有一个,因此需要和这一个副本所在的 rack 一致)。

topic0-1的第一个分区的分配流程:

由于consumer 1 所在的rack: rack 0 和 topic 0 以及 topic 1 的 partition 0 所在的rack是一样的,因此我们会将topic 0 - parititon 0 以及 topic 1 - parititon 0 都分配给 consumer 1。之后判断consumer 1 对于 topic 0 和 topic 1 的分区的分配数量是否已经到了上限。具体上限的计算公式为:

topic 0 总共有3个分区,两个消费者,那么每个消费者平均有 3(分区数) / 2(consumer数) = 1个分区,剩余1个分区(3%2),剩余的分区按照消费者的先后顺序分配,排在前面的优先分配,因此 conusmer 0 最多可以分配到 topic 0 的 2个分区。类似的,consumer 1 最多可以分配到 topic 0 的 1个分区。 topic 1 因为分区数量和topic 0 一样,所以每个消费者能分到的最大分区数量也是一样的。

如果达到了上限,后面再分配分区就直接跳过此consumer。对于,consumer 1,它已经分配不到分区了。

topic0-1的第二个分区的分配流程:

由于consumer 0 所在的rack: rack 0 和 topic 0 以及 topic 1 的 partition 1 所在的rack都不一样,因此这里不将 partition 1 分配给 consumer 0。而consumer 1 也是类似的情况,所以两个topic的partition 1 不会分配给任何一个consumer。

topic0-1的第三个分区的分配流程:

与第二个分区情况类似,所以两个topic的partition 2 也不会分配给任何一个consumer。

综上所述,Rack Aware机制还是比较严格的,如果一个consumer订阅了多个topic,而多个topic的分区数量又一样的话,每个topic同样编号的分区要想分配给该consumer,此conusmer所在的rack必须要和这些同样编号的分区的所有副本所在的rack中至少有一个是相同的。其中有哪一个topic的分区的副本的rack中没有和该conusmer的rack一样,就不会分配给它任何一个topic的对应编号的分区。

后续再分配

经过Rack Aware分配历程后,consumer 1 分配到了 topic0-1的 partition 0,对于topic0-1,分别都能再分到1个分区。 conusmer 0 则是一无所获。 接下来则是按照此前说明的未启用Rack Aware时的分配流程来进行分配剩下的分区了。相对来说简单的多,最终可以得到如下结果:

kafka-Range-rack-aware分区分配结果.drawio.png

RoundRobinAssignor

此分配策略跨越多个topic,针对多个topic的多个parttion分配。RoundRobinAssignor的分配策略非常简单,正如其名字一样,采用轮询的方式给consumer分配分区,对排好顺序的分区依次分配,当某个consumer没有订阅此分区所属的topic时,会直接跳过,尝试给下一个consumer分配。

订阅情况

kafka-round-robin.drawio.png

排序

RangeAssignor策略一样,首先也是需要对consumer和partition排序。

consumer: conusmer 0, conusmer 1

partition: topic 0 - partition 0, topic 0 - partition 1, topic 0 - partition 2, topic 1 - partition 0, topic 1 - partition 1, topic 1 - partition 2.

分配流程

  • topic 0 - partition 0: consumer 0 排在前,且订阅了topic 0, 因此此分区分配给consumer 0
  • topic 0 - partition 1: 此时轮到给consumer 1 分配分区了,由于consumer 1 也订阅了topic 0,因此该分区分配给conusmer 1
  • topic 0 - partition 2: 轮到consumer 0 了,consumer 0 订阅了topic 0, 因此此分区分配给consumer 0
  • topic 1 - partition 0: 轮到conusmer 1了,consumer 1 没有订阅topic 1,因此直接跳过
  • topic 1 - partition 0: 轮到conusmer 0了,consumer 0 订阅了topic 1,因此得到了这个分区
  • topic 1 - partition 1: 轮到conusmer 1了,consumer 1 没有订阅topic 1,因此直接跳过
  • topic 1 - partition 1: 轮到conusmer 0了,consumer 0 订阅了topic 1,因此得到了这个分区
  • topic 1 - partition 2: 轮到conusmer 1了,consumer 1 没有订阅topic 1,因此直接跳过
  • topic 1 - partition 2: 轮到conusmer 0了,consumer 0 订阅了topic 1,因此得到了这个分区

最终的分配结果如下:

kafka-round-robin-分配结果.drawio.png

缺点

可以看到此种分配方式下,consumer 0 比 consumer 1多了太多的分区(5:1)。

StickyAssignor

目标:

  • 同rack优先
  • 优先分配之前分配到的分区,避免分区移动带来的花费
  • 尽量保证consumer分配到的分区数量差异在一个以内。

针对消费者订阅的topic是否完全相同,有两种策略:

  • 完全相同:ConstrainedAssignmentBuilder
  • 不完全相同:GeneralAssignmentBuilder

ConstrainedAssignmentBuilder

此分配算法的大致流程是:

  1. 分配给consumer上次Reblance分配到的分区,如果有的话
  2. 如果满足启用Rack Aware机制的条件,则将剩下的分区按照同Rack优先的原则结合Round Robin算法进行分配
  3. 最后剩下的分区按照普通的Round Robin算法进行分配

排序

字典序从小到大排:

consumer: conusmer 0,conusmer 1

partition需要先排序(先按照topic的名字字典排序,接着按照partition的编号从小到大排):

partition: topic 0 - partition 0, topic 0 - partition 1, topic 0 - partition 2, topic 1 - partition 0, topic 1 - partition 1.

相关指标和数据结构

分区最小配额(minQuota):floor(count(partition) / count(consumer))

分区最大配额(maxQuota):ceil(count(partition) / count(consumer))

已分配分区数量没有达到最小配额的消费者集合:unfilledMembersWithUnderMinQuotaPartitions

已分配分区数量正好达到最小配额的消费者集合:unfilledMembersWithExactlyMinQuotaPartitions

未启用Rack Aware,所有消费者之前都没有被分配过分区

订阅情况

kafka-sticky-round-robin-订阅情况.drawio.png

那么,有:

minQuota: floor(5/2=2.5) = 2

maxQuota: ceil(5/2=2.5) = 3

unfilledMembersWithUnderMinQuotaPartitions = [consumer 0, consumer 1]

分配流程
  • topic 0 - partition 0: consumer 0 获得该分区,已分配分区数量:1

  • topic 0 - partition 1: consumer 1 获得该分区,已分配分区数量:1

  • topic 0 - partition 2: consumer 0 获得该分区,已分配分区数量:2,达到分区最小配额,需要从unfilledMembersWithUnderMinQuotaPartitions集合中移除此消费者,同时在unfilledMembersWithExactlyMinQuotaPartitions集合加入此消费者

  • topic 1 - partition 0: consumer 1 获得该分区,已分配分区数量:2,达到分区最小配额,需要从unfilledMembersWithUnderMinQuotaPartitions集合中移除此消费者,同时在unfilledMembersWithExactlyMinQuotaPartitions集合加入此消费者

  • topic 1 - partition 1: 由于unfilledMembersWithUnderMinQuotaPartitions集合已经为空,从unfilledMembersWithExactlyMinQuotaPartitions集合取消费者,这里按照先进先出的原则,先取出conusmer 0,并让consumer 0 获取到此分区

分配结果:

kafka-sticky-round-robin-分配结果.drawio.png

可以看到基本逻辑非常简单,经典的Round Robin,并且由于consumer订阅的topic都相同,在Round Robin算法下,可以保证消费者最终分配得到的分区数量最多差1个。之所以还有两个集合,是因为后续如果存在有消费者上一轮分区Reblance时分配过分区或者可以使用Rack Aware机制时,还需要用到这两个集合。

启用Rack Aware,所有消费者之前都没有被分配过分区

Rack Aware机制下,算法会优先将分配的分区数量达到最小配额的consumer的分区数量扩充到最大配额,或者尽量达到最小配额为止。

订阅情况

kafka-sticky-round-robin-订阅情况.drawio.png

那么,有:

minQuota: floor(5/2=2.5) = 2

maxQuota: ceil(5/2=2.5) = 3

unfilledMembersWithUnderMinQuotaPartitions = [consumer 0, consumer 1]

Rack示意图

kafka-sticky-round-robin-rack-aware.drawio.png

分区重排序

理由:

Return a sorted linked list of partitions to enable fast updates during rack-aware assignment

各个分区需要按照其所有副本所在的Rack中存在的consumer的数量从少到多排(Java的Collections.sort排序是稳定排序算法,因此数量相同时,顺序按照之前的基于topic字典序排序及分区编号大小排序后的顺序来,因此有如下排序结果):

  1. topic 0 - partition 2: 0

  2. topic 1 - partition 1: 0

  3. topic 0 - partition 0: 1 (consumer 1)

  4. topic 0 - partition 1: 1 (consumer 0)

  5. topic 1 - partition 0: 1 (consumer 1)

分配流程

分配流程分为两个阶段,第一个阶段基于Rack Aware机制进行分配,第二个阶段采用简单的Round Robin算法分配剩下的分区。

基于Rack机制的Round Robin分配
集合consumers
unfilledMembersWithUnderMinQuotaPartitions[consumer 0, consumer 1]
unfilledMembersWithExactlyMinQuotaPartitions[]
  1. topic 0 - partition 2: 从unfilledMembersWithUnderMinQuotaPartitions集合中索引为 0 的位置开始循环搜索第一个满足所在的Rack存在任一个此分区副本的consumer,由于consumer 0和 consumer 1都不满足条件,所以这个分区暂时忽略,在后续的分配流程中继续分配。下一次从unfilledMembersWithUnderMinQuotaPartitions集合中索引为 0 的位置开始搜索consumer

    集合consumers
    unfilledMembersWithUnderMinQuotaPartitions[consumer 0, consumer 1]
    unfilledMembersWithExactlyMinQuotaPartitions[]
  2. topic 1 - partition 1: 从unfilledMembersWithUnderMinQuotaPartitions集合中索引为 0 的位置开始循环搜索第一个满足所在的Rack存在任一个此分区副本的consumer,由于consumer 0和 consumer 1都不满足条件,所以这个分区暂时忽略,在后续的分配流程中继续分配。下一次从unfilledMembersWithUnderMinQuotaPartitions集合中索引为 0 的位置开始搜索consumer。

    集合consumers
    unfilledMembersWithUnderMinQuotaPartitions[consumer 0, consumer 1]
    unfilledMembersWithExactlyMinQuotaPartitions[]
  3. topic 0 - partition 0: 从unfilledMembersWithUnderMinQuotaPartitions集合中第索引为 0 的位置开始循环搜索第一个满足所在的Rack存在任一个此分区副本的consumer,这里consumer 0不满足条件,轮换到 consumer 1。分配此分区给该consumer,已获取分区数量:1。下一次从unfilledMembersWithUnderMinQuotaPartitions集合中索引为 0 的位置开始搜索consumer

    集合consumers
    unfilledMembersWithUnderMinQuotaPartitions[consumer 0, consumer 1]
    unfilledMembersWithExactlyMinQuotaPartitions[]
  4. topic 0 - partition 1: 从unfilledMembersWithUnderMinQuotaPartitions集合中索引为0 的位置开始搜索,consumer 0恰好满足条件,将此分区分配给consumer 0, 已获取分区数量:1。下一次从unfilledMembersWithUnderMinQuotaPartitions集合中索引为 1 的位置开始搜索consumer

    集合consumers
    unfilledMembersWithUnderMinQuotaPartitions[consumer 0, consumer 1]
    unfilledMembersWithExactlyMinQuotaPartitions[]
  5. topic 1 - partition 0:从unfilledMembersWithUnderMinQuotaPartitions集合索引为 1 的位置开始搜索,consumer 1正好满足条件,分配此分区给该consumer,已获取分区数量:2。达到分区最小配额,需要从unfilledMembersWithUnderMinQuotaPartitions集合中移除此消费者,同时在unfilledMembersWithExactlyMinQuotaPartitions集合加入此消费者。下一次从unfilledMembersWithUnderMinQuotaPartitions集合第 0 个位置开始搜索consumer

    集合consumers
    unfilledMembersWithUnderMinQuotaPartitions[consumer 0]
    unfilledMembersWithExactlyMinQuotaPartitions[consumer 1]

分配结果:

kafka-sticky-rack-aware-分配结果.drawio.png

Round Robin

集合情况

集合consumers
unfilledMembersWithUnderMinQuotaPartitions[consumer 0]
unfilledMembersWithExactlyMinQuotaPartitions[consumer 1]

分配情况

consumerconsumers分区数量
consumer 0topic 0 - partition 11
consumer 1[topic 0 - partition 0, topic 1 - partition 0]2

上一个流程分配剩下的分区为topic 0 - partition 2,topic 1 - partition 1,在此流程中继续分配它们。

  1. topic 0 - partition 2: 从unfilledMembersWithUnderMinQuotaPartitions集合中取出第一个consumer,这里只有一个consumer 0,分配此分区给该consumer,已分配分区数量:2,达到最小限额,从unfilledMembersWithUnderMinQuotaPartitions集合中移除此消费者,同时在unfilledMembersWithExactlyMinQuotaPartitions集合加入此消费者。

    集合consumers
    unfilledMembersWithUnderMinQuotaPartitions[]
    unfilledMembersWithExactlyMinQuotaPartitions[consumer 1, consumer 0]
  2. topic 1 - partition 1: 从unfilledMembersWithUnderMinQuotaPartitions集合中已经没有consumer,需要从unfilledMembersWithExactlyMinQuotaPartitions集合中取出第一个消费者,这里是consumer 1,已分配分区数量:3,已达最大配额。

    集合consumers
    unfilledMembersWithUnderMinQuotaPartitions[]
    unfilledMembersWithExactlyMinQuotaPartitions[consumer 0, consumer 1]

最终的分配结果:

kafka-1-stick-rack-aware-分配结果-1.drawio.png

启用Rack Aware,消费者之前有被分配过分区的

在进行具体分析之前,要先说明一个概念:Generation ID。Generation ID 是一个​​单调递增的整数​​,由消费者组的协调器(Group Coordinator)在每次触发再平衡时分配并更新,它代表消费者组的“代数”,用于区分再平衡前后的组状态,确保组内成员的操作基于最新状态,有如下核心功能:

  1. ​防止过期操作​​:拒绝来自前代成员的延迟请求(如偏移量提交),避免数据不一致
  2. ​避免脑裂问题​​:确保同一时刻仅有一个有效generation被协调器认可,防止多个消费者组状态冲突
  3. ​保障再平衡原子性​​:每次再平衡会生成全新的组“快照”,前代的操作立即失效

Map<String, Subscription> subscriptions中保存了此次分区分区所涉及到的消费组的成员和它们的订阅情况,Subscription中定义了名为userData的成员变量,在StickyAssignor策略下,我们可以从userData中获取到当前consumer的generatation id以及目前分配给它的分区(这些分区在上一次分区分配时获得)。StickyAssignor策略会尝试将这些分区在这次分区分配时还是分配给它们,这样可以避免分区移动带来的资源耗费(分区移动指的是将其他消费者之前拥有的分区分配给新的消费者)。

对消费组中的所有消费者上次分配获取的分区要进行如下预处理: 如果某个分区被多个consumer拥有,则generatation id更大的consumer会继续拥有该分区(此种情况可能是某个前代的因为心跳超时导致被踢出消费组的consumer又重新加入消费组了),如果generatation id相同,则说明分区分配算法出现了异常,需要将这个分区加入到partitionsWithMultiplePreviousOwners集合中去。

通过此预处理,我们得到了如下数据结构:

数据结构定义作用
consumerToOwnedPartitionsMap<String, List>保存consumer和它上次分配分到的分区
partitionsWithMultiplePreviousOwnersSet保存多个同代(generation id 一样)的consumer上一次分配时所共同拥有的分区

在上述数据结构的基础上,开始尝试给consumer分配它们上次分配时分配到的分区。详细过程如下:

kafka-sticky-owned-partitions-1.drawio.png

以下,举例开始进行演示分区分配。

订阅情况

kafka-1.drawio.png

那么,有:

minQuota: floor(3/2=1.5) = 1

maxQuota: ceil(5/2=1.5) = 2

unfilledMembersWithUnderMinQuotaPartitions = [consumer 0, consumer 1]

Rack示意图

kafka-4.drawio.png

上次分配分区情况
consumerowned partitions
consumer 0[topic 0 - partition 0, topic 0 - partition 1, topic 1 - partition 0]
consumer 1[]
分配已有分区(owned partitions)

对于consumer 0,其现在有topic 0 - partition 0, topic 0 - partition 1, topic 1 - partition 0三个分区,对这些分区做如下处理:

  1. 过滤掉所有副本都不在consumer 0 所在的rack上的分区。这里将过滤掉topic 0 - partition 1。
  2. 剩下两个分区,数量等于最大配额(2),这里可以正好保留这两个分区,将它们分配给consumer 0。

对于consumer 1,它此前没有被分配过分区(可能是这一代刚加入的consumer),这里只需要将其加入到unfilledMembersWithUnderMinQuotaPartitions集合中即可。

基于Rack Aware 机制的 Round Robin分配
分区重排序

由于只剩一个分区,所以排序结果就是

  1. topic 0 - partition 1。
分配流程
集合consumers
unfilledMembersWithUnderMinQuotaPartitions[consumer 1]
unfilledMembersWithExactlyMinQuotaPartitions[]

topic 0 - partition 1: 从unfilledMembersWithUnderMinQuotaPartitions集合中索引为 0 的位置开始循环搜索第一个满足所在的Rack存在任一个此分区副本的consumer,consumer 1正好满足条件,将其分配给该消费者,已分配分区数量:1。达到分区最小配额(1),需要从unfilledMembersWithUnderMinQuotaPartitions集合中移除此消费者,同时在unfilledMembersWithExactlyMinQuotaPartitions集合加入此消费者

集合consumers
unfilledMembersWithUnderMinQuotaPartitions[]
unfilledMembersWithExactlyMinQuotaPartitions[consumer 1]
Round Robin分配

由于所有分区已经分配完毕,所以这个流程什么也不用干。

最终的分配结果

kafka-2.drawio.png

GeneralAssignmentBuilder

排序

consumer排序

对于consumer的排序,需要先按照consumer已分配分区的数量从少到多排,如果数量相同,则按照member.id字典序从前往后排。

partition排序

对于partition的排序,需要先按照topic排序,订阅topic的consumer的数量越少排在越前面,如果数量相同,则按照topic名字的字典序进行排序。

再对topic内的partition排序(事实上在执行分区策略前,partition已经按照编号排过序了)。

记录前代consumer分区的分配情况

此流程需要记录前代消费者分配过了哪些分区,如果某个分区被多个generation的consumer分配过,则让generation更大的consumer记录下分配关系。这个记录留待后续分区再分配时使用。

分配已有分区

这一步的流程是将consumer上次分配到的分区尽可能地保存下来,如果consumer所在的rack上不存在某个分区任何一个副本,则将此分区直接移除掉。

未分配分区继续分配

此流程分为两步:

  1. 基于Rack Aware机制进行分配,如果某个consumer所在的rack上存在分区的任一个副本,那么就将此分区分配给这个consumer

  2. 余下分区只要满足分区所属topic被某个consumer订阅,即可分配

至此所有分区都完成了分配。

分区再分配

该流程目的是让全部消费者分区数量达到最佳平衡状态。

partition过滤:先过滤掉所属topic订阅consumer数量小于2的分区,此种分区没法再转给其他consumer;

consumer过滤:如果一个consumer当前所分配到的分区所属的topic订阅的consumer数量都不超过两个的话,直接过滤掉,这种consumer没法把自己的分区转移给其他consumer,也没办法接受其他consumer转过来的分区。

这些被过滤掉的consumer和它分配的分区被放到一个称为固定分配的集合中,后续还有用。

具体流程

kafka-6.drawio.png

如何判断各个消费者分配到的分区数量是否均衡?

TreeSet<String> sortedCurrentSubscriptions: 保存了一个消费者顺序集合,消费者按照分配到分区数量从小到大进行排序,如果数量相同则按照消费者member.id字典排序

  1. 从此结构中拿出第一个consumer和最后一个consumer,如果它们当前分配到的分区数量最多只差一个的话,说明整体平衡
  2. 对于consumer a 和 consumer b,如果发现一个partition已经被分配到consumer b,且consumer b分配到的分区数量比consumer a的要多至少一个,意味着我们可以在后续流程中将这个partition重新分配给consumer a,这种操作称之为分区移动。如果存在这种partition的话,说明consumer整体分区分配尚不平衡。
  3. 以上情况都不存在的话,则认为整体平衡

判断是否应用再分配的结果

这里只需要比较再分配前后的Blance Score大小即可,越平衡的分配方式Blance Score越小,为0时达到所有consumer分配分区数量完全一致的理想状态。Blance Score的计算方式很简单,将每个consumer和其他consumer分配的分区数量差异累加即可。

固定分配加入到现有的分配集合中

将之前流程中被过滤掉的consumer及其分配分区融入到目前的分配方案中。

CooperativeStickyAssignor

此分配策略是在StickyAssignor分配完分区的基础上做一些调整:如果某些分区原本属于某些consumer,但是这次Reblance被分配给了其他的consumer,那么将这些分区从分配给这些其他的consumer的分区集合中移除。当最终的分配方案被Group Coordinator发送给消费组内的所有consumer时,每个consumer检视新的分配方案,如果方案中不包含现有的某些分区,就需要将这些分区放弃/撤销(Revoke)掉。这个调整正如本文一开始说的那样:分配者不应该立即重新分配任何拥有的分区,而是可以指示消费者需要撤销分区,以便可以在下一次重新平衡事件中将被撤销的分区重新分配给其他消费者