Kafka 分区分配策略

Kafka 默认分区分配策略

1 consumer 订阅 1 topic (7 partition)

按照 Kafka 默认的消费逻辑设定,一个分区只能被同一个消费组(ConsumerGroup)内的一个消费者消费。

假设目前某消费组内只有一个消费者 C0,订阅了一个 topic,这个 topic 包含 7 个分区,也就是说这个消费者 C0 订阅了 7 个分区,参考下图。

1.png

2 consumer 订阅 1 topic (7 partition)

此时消费组内又加入了一个新的消费者 C1,按照既定的逻辑需要将原来消费者 C0 的部分分区分配给消费者 C1 消费,情形如下图,消费者 C0 和 C1 各自负责消费所分配到的分区,相互之间并无实质性的干扰。

2.png

3 consumer 订阅 1 topic (7 partition)

接着消费组内又加入了一个新的消费者 C2,如此消费者 C0、C1 和 C2 按照上图(3)中的方式各自负责消费所分配到的分区。

3.png

8 consumer 订阅 1 topic (7 partition)

如果消费者过多,出现了消费者的数量大于分区的数量的情况,就会有消费者分配不到任何分区。参考下图,一共有 8 个消费者,7 个分区,那么最后的消费者 C7 由于分配不到任何分区进而就无法消费任何消息。

4.png

上面各个示例中的整套逻辑是按照 Kafka 中默认的分区分配策略来实施的。Kafka 提供了消费者客户端参数 partition.assignment.strategy 用来设置消费者与订阅主题之间的分区分配策略。

默认情况下,此参数的值为:org.apache.kafka.clients.consumer.RangeAssignor,即采用 RangeAssignor 分配策略。除此之外,Kafka 中还提供了另外两种分配策略: RoundRobinAssignor 和 StickyAssignor。消费者客户端参数 partition.asssignment.strategy 可以配置多个分配策略,彼此之间以逗号分隔。

#轮询策略设置

partition.assignment.strategy=org.apache.kafka.clients.consumer.RoundRobinAssignor

#范围策略设置

partition.assignment.strategy=org.apache.kafka.clients.consumer.RangeAssignor

#粘性策略设置

partition.assignment.strategy=org.apache.kafka.clients.consumer.StickyAssignor

属于同一组的所有使用者必须声明一个共同的策略。如果使用者尝试以与其他组成员不一致的分配配置加入一个组,您将最终遇到以下异常:

org.apache.kafka.common.errors.InconsistentGroupProtocolException: The group member’s supported protocols are incompatible with those of existing members or first group member tried to join with empty protocol type or empty protocol list.

此属性接受以逗号分隔的策略列表。例如,它允许您通过指定新策略来更新一组使用者,同时暂时保留前一个。作为重新平衡协议的一部分,经纪人协调员将选择所有成员都支持的协议。

RangeAssignor 分配策略

RangeAssignor 策略的原理是按照消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配,以保证分区尽可能均匀地分配给所有的消费者。对于每一个 topic,RangeAssignor 策略会将消费组内所有订阅这个 topic 的消费者按照名称的字典序排序,然后为每个消费者划分固定的分区范围,如果不够平均分配,那么字典序靠前的消费者会被多分配一个分区。

① 假设 n= 一个 topic 的分区数 / 订阅此 topic 消费者数量,
② m= 分区数 % 消费者数量,
③ 那么前 m 个消费者每个分配 n+1 个分区,后面的(消费者数量 -m)个消费者每个分配 n 个分区。

假设一个 topic 有 13 分区,订阅此 topic 有 3 个消费者 C0、C1、C2,那么每个消费者先分配 13 / 3 = 4 个分区,剩下 1 (13 % 3 =) 个分区,给第一个消费者 C0。
此时各个消费则分配 topic 的分区如下:
C0 : 4 + 1 个
C1 : 4 个
C2 : 4 个

为了更加通俗的讲解 RangeAssignor 策略,我们不妨再举一些示例。
假设消费组内有 2 个消费者 C0 和 C1,都订阅了主题 t0 和 t1,并且每个主题都有 4 个分区,那么所订阅的所有分区可以标识为:t0p0、t0p1、t0p2、t0p3、t1p0、t1p1、t1p2、t1p3。最终的分配结果为:

5.png

消费者 C0:t0p0、t0p1、t1p0、t1p1
消费者 C1:t0p2、t0p3、t1p2、t1p3

这样分配的很均匀,那么此种分配策略能够一直保持这种良好的特性呢?我们再来看下另外一种情况。假设上面例子中 2 个主题都只有 3 个分区,那么所订阅的所有分区可以标识为:t0p0、t0p1、t0p2、t1p0、t1p1、t1p2。最终的分配结果为:

6.png
消费者 C0:t0p0、t0p1、t1p0、t1p1
消费者 C1:t0p2、t1p2

可以明显的看到这样的分配并不均匀,如果将类似的情形扩大,有可能会出现部分消费者过载的情况。对此我们再来看下另一种 RoundRobinAssignor 策略的分配效果如何。

RoundRobinAssignor 分配策略

RoundRobinAssignor 策略的原理是将消费组内所有消费者以及消费者所订阅的所有 topic 的 partition 按照字典序排序,然后通过轮询消费者方式逐个将分区分配给每个消费者。RoundRobinAssignor 策略对应的 partition.assignment.strategy 参数值为:org.apache.kafka.clients.consumer.RoundRobinAssignor。

#### 消费者订阅相同 Topic

如果同一个消费组内所有的消费者的订阅信息都是相同的,那么RoundRobinAssignor策略的分区分配会是均匀的。

举例,假设消费组中有 2 个消费者 C0 和 C1,都订阅了主题 t0 和 t1,并且每个主题都有 3 个分区,那么所订阅的所有分区可以标识为:t0p0、t0p1、t0p2、t1p0、t1p1、t1p2。最终的分配结果为:

7.png
消费者 C0:t0p0、t0p2、t1p1
消费者 C1:t0p1、t1p0、t1p2

消费者订阅不同 Topic

如果同一个消费组内的消费者所订阅的 Topic 是不相同的,那么在执行分区分配的时候就不是完全的轮询分配,有可能会导致分区分配的不均匀。如果某个消费者没有订阅消费组内的某个 topic,那么在分配分区的时候此消费者将分配不到这个 topic 的任何分区。

举例,假设消费组内有 3 个消费者 C0、C1 和 C2,它们共订阅了 3 个主题:t0、t1、t2,这 3 个主题分别有 1、2、3 个分区,即整个消费组订阅了 t0p0、t1p0、t1p1、t2p0、t2p1、t2p2 这 6 个分区。

8.png

具体而言,消费者 C0 订阅的是主题 t0,消费者 C1 订阅的是主题 t0 和 t1,消费者 C2 订阅的是主题 t0、t1 和 t2,那么最终的分配结果为:

9.png

消费者 C0:t0p0
消费者 C1:t1p0
消费者 C2:t1p1、t2p0、t2p1、t2p2

可以看到 RoundRobinAssignor 策略也不是十分完美,这样分配其实并不是最优解,因为完全可以将分区 t1p1 分配给消费者 C1,如下图:

10.png

StickyAssignor 分配策略

我们再来看一下 StickyAssignor 策略,“sticky”这个单词可以翻译为“粘性的”,Kafka 从 0.11.x 版本开始引入这种分配策略,它主要有两个目的:

① 分区的分配要尽可能的均匀;
② 分区的分配尽可能的与上次分配的保持相同。

当两者发生冲突时,第一个目标优先于第二个目标。鉴于这两个目标,StickyAssignor 策略的具体实现要比 RangeAssignor 和 RoundRobinAssignor 这两种分配策略要复杂很多。我们举例来看一下 StickyAssignor 策略的实际效果。

消费者订阅相同 Topic

假设消费组内有 3 个消费者:C0、C1 和 C2,它们都订阅了 4 个主题:t0、t1、t2、t3,并且每个主题有 2 个分区,也就是说整个消费组订阅了 t0p0、t0p1、t1p0、t1p1、t2p0、t2p1、t3p0、t3p1 这 8 个分区。最终的分配结果如下:

11.png

消费者 C0:t0p0、t1p1、t3p0
消费者 C1:t0p1、t2p0、t3p1
消费者 C2:t1p0、t2p1

这样初看上去似乎与采用 RoundRobinAssignor 策略所分配的结果相同,但事实是否真的如此呢?再假设此时消费者 C1 脱离了消费组,那么消费组就会执行再平衡操作,进而消费分区会重新分配。如果采用 RoundRobinAssignor 策略,那么此时的分配结果如下:

12.png

消费者 C0:t0p0、t1p0、t2p0、t3p0
消费者 C2:t0p1、t1p1、t2p1、t3p1

如分配结果所示,RoundRobinAssignor 策略会按照消费者 C0 和 C2 进行重新轮询分配。而如果此时使用的是 StickyAssignor 策略,那么分配结果为:

13.png
消费者 C0:t0p0、t1p1、t3p0、t2p0
消费者 C2:t1p0、t2p1、t0p1、t3p1
可以看到分配结果中保留了上一次分配中对于消费者 C0 和 C2 的所有分配结果,并将原来消费者 C1 的“负担”分配给了剩余的两个消费者 C0 和 C2,最终 C0 和 C2 的分配还保持了均衡。

如果发生分区重分配,那么对于同一个分区而言有可能之前的消费者和新指派的消费者不是同一个,对于之前消费者进行到一半的处理还要在新指派的消费者中再次复现一遍,这显然很浪费系统资源。StickyAssignor 策略如同其名称中的“sticky”一样,让分配策略具备一定的“粘性”,尽可能地让前后两次分配相同,进而减少系统资源的损耗以及其它异常情况的发生。

消费者订阅不同 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。

如果此时采用 RoundRobinAssignor 策略,那么最终的分配结果如下所示(和讲述 RoundRobinAssignor 策略时的一样,这样不妨赘述一下):
(红线是订阅,其他颜色的线是分配分区)

14.png

【分配结果集 1】
消费者 C0:t0p0
消费者 C1:t1p0
消费者 C2:t1p1、t2p0、t2p1、t2p2

如果此时采用的是 StickyAssignor 策略,那么最终的分配结果为:
(红线是订阅,其他颜色的线是分配分区)

15.png

【分配结果集 2】
消费者 C0:t0p0
消费者 C1:t1p0、t1p1
消费者 C2:t2p0、t2p1、t2p2
可以看到这是一个最优解(消费者 C0 没有订阅主题 t1 和 t2,所以不能分配主题 t1 和 t2 中的任何分区给它,对于消费者 C1 也可同理推断)。

消费者脱离消费组的情况 RoundRobin

假如此时消费者 C0 脱离了消费组,那么 RoundRobinAssignor 策略的分配结果为:
(红线是订阅,其他颜色的线是分配分区)

16.png

消费者 C1:t0p0、t1p1
消费者 C2:t1p0、t2p0、t2p1、t2p2
可以看到 RoundRobinAssignor 策略保留了消费者 C1 和 C2 中原有的 3 个分区的分配:t2p0、t2p1 和 t2p2(针对结果集 1, 保留了三个绿色的, 结果集 1 如下图,做参照)。

17.png

消费者脱离消费组的情况 Sticky

而如果采用的是 StickyAssignor 策略,那么分配结果为:
(红线是订阅,其他颜色的线是分配分区)

18.png

消费者 C1:t1p0、t1p1、t0p0
消费者 C2:t2p0、t2p1、t2p2

可以看到 StickyAssignor 策略保留了消费者 C1 和 C2 中原有的 5 个分区的分配:t1p0、t1p1、t2p0、t2p1、t2p2。(针对结果集 2, 保留了三个绿色的, 结果集 2 如下图,做参照)。
19.png

从结果上看 StickyAssignor 策略比另外两者分配策略而言显得更加的优异,这个策略的代码实现也是异常复杂,如果大家在一个 group 里面,不同的 Consumer 订阅不同的 topic, 那么设置 Sticky 分配策略还是很有必要的。

springboot 使用 kafka