Kafka-消费者组三种分区分配策略Range Assignor、RoundRobin Assignor、Sticky Assignor详细解析

Kafka消费者组三种分区分配策略roundrobin,range,StickyAssignor

一个consumer group中有多个consumer,一个 topic有多个partition,所以必然会涉及到partition的分配问题,即确定那个partition由哪个consumer来消费。

Kafka有三种分配策略,roundrobin Assignor,range Assignor,Sticky Assignor

将分区的所有权从一个消费者移到另一个消费者称为重新平衡(rebalance)。当以下事件发生时,Kafka 将会进行一次分区分配:

  • 同一个 Consumer Group 内新增消费者
  • 消费者离开当前所属的Consumer Group,包括shuts down 或 crashes
  • 订阅的主题新增分区

目前还不能自定义分区分配策略,只能通过partition.assignment.strategy参数,设置 消费者 与 订阅主题 之间的分区分配策略,默认使用 RangeAssignor 分配策略,此参数partition.asssignment.strategy可以配置多个分配策略,彼此之间以逗号分隔。

1. Range(默认策略)

2.4.x版本官方介绍:

http://kafka.apache.org/24/javadoc/org/apache/kafka/clients/consumer/RangeAssignor.html

Range:是以Topic为单位,首先,对同一个Topic里的分区,按照序号进行排序,并对消费者按照字母顺序进行排序。然后用Partitions分区的个数除以消费者线程的总数来决定每个消费者线程消费几个分区。如果除不尽,那么前面几个消费者线程将会多消费一个分区。

大致估算每个消费者分配多少个分区 n = 分区数/消费者数量(n:分配的分区个数)

若除不尽,则取余数 m = 分区数 % 消费者数量,表示前 m 个消费者每个分配 n+1 个分区(即多分配一个分区),剩余的消费者(消费者数量 减去 m)每个分配 n 个分区。

有10个分区,3个消费者线程,把分区按照序号排列0,1,2,3,4,5,6,7,8,9;消费者线程为C1-0,C2-0,C2-1。

例如,有10个分区,3个消费者线程,10/3 = 3,除不尽,10%3=1,那么消费者线程C1-0将会多消费一个分区:

C1-0:0,1,2,3
C2-0:4,5,6
C2-1:7,8,9

如果有11个分区,则11%3=2,表示前面两个消费者多分配一个分区:
C1-0:0,1,2,3
C2-0:4,5,6,7
C2-1:8,9,10

假如有两个主题T1,T2,分别有10个分区,最后的分配结果将是这样:

C1-0:T1(0,1,2,3) T2(0,1,2,3)
C2-0:T1(4,5,6) T2(4,5,6)
C2-1:T1(7,8,9) T2(7,8,9)

可以看出, C1-0消费者线程比其他消费者线程多消费了2个分区

如上,只是针对 1 个 topic 而言,C1-0消费者多消费1个分区影响不是很大。如果有 N 多个 topic,那么针对每个 topic,消费者 C1-0 都将多消费 1 个分区,topic越多,C1-0 消费的分区 会比其他消费者 明显多消费 N 个分区。这就是 Range 范围分区的一个很明显的弊端了

2. RoundRobin

2.4.x版本官方介绍:

http://kafka.apache.org/24/javadoc/org/apache/kafka/clients/consumer/RoundRobinAssignor.html

RoundRobin是以消费者组为单位,将消费者组内 所有消费者 以及消费者订阅的 所有topic的分区 按字典序排序,然后通过轮询方式,逐个将分区分配给每个消费者。

使用RoundRobin策略有两个前提条件必须满足:

  1. 同一个消费者组里的 所有消费者的num.streams(消费者消费线程数)必须相等;
  2. 每个消费者订阅的主题必须相同。

假设2个消费者的num.streams = 2。RoundRobin策略的工作原理:将所有主题的分区组成 TopicAndPartition 列表,然后对TopicAndPartition 列表按照 hashCode 进行排序,最后按照roundrobin模式,将分区分别分配给不同的消费者线程。

val allTopicPartitions = ctx.partitionsForTopic.flatMap { case(topic, partitions) =>
  info("Consumer %s rebalancing the following partitions for topic %s: %s"
       .format(ctx.consumerId, topic, partitions))
  partitions.map(partition => {
    TopicAndPartition(topic, partition)
  })
}.toSeq.sortWith((topicPartition1, topicPartition2) => {
  /*
   * Randomize the order by taking the hashcode to reduce the likelihood of all partitions of a given topic ending
   * up on one consumer (if it has a high enough stream count).
   */
  topicPartition1.toString.hashCode < topicPartition2.toString.hashCode
})

RoundRobin的两种情况

  1. 如果同一个消费组内 所有的消费者订阅的topic 都是一样的,那么RoundRobinr策略的分区分配是均匀的。

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

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

  1. 如果同一个消费组内的消费者 所订阅的topic 是不一样的,那么在执行分区分配的时候,就不是完全的轮询分配,有可能会导致分区分配的不均匀。 如果某个消费者 没有订阅消费组内的某个topic,那么在分配分区的时候,此消费者将分配不到这个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,那么最终的分配结果为:

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

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

3. StickyAssignor

2.4.x版本官方介绍:

http://kafka.apache.org/24/javadoc/org/apache/kafka/clients/consumer/StickyAssignor.html

StickyAssignor黏性分区,是0.11.x版本引入的新分配策略,它主要有两个目的:

  1. 分区的分配要尽可能的均匀,分配给消费者者的主题分区数最多相差一个;
  2. 分区的分配尽可能的与上次分配的保持相同。

当两者发生冲突时,第一个目标优先于第二个目标。

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

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

这样看上去似乎与采用RoundRobinAssignor策略所分配的结果相同,但实际并非如此

此时假设消费者C1脱离了消费组,那么消费组就会执行再平衡操作,进而消费分区会重新分配。如果采用RoundRobin策略,那么此时的分配结果如下:

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

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

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

可以看到分配结果中,保留了上一次分配中对于消费者C0和C2的所有分配结果,并将原来消费者C1消费的分区,分配给了剩余的两个消费者C0和C2,最终C0和C2的分配还保持了均衡。

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

若组内消费者订阅的主题不一样的情况如下:

举例,同样消费组内有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策略时的一样):

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

如果采用的是StickyAssignor策略,那么最终的分配结果为:

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

可以看到这是一个最优解(消费者C0没有订阅主题t1和t2,所以不能分配主题t1和t2中的任何分区给它,对于消费者C1也可同理推断)。

假如此时消费者C0脱离了消费组,那么RoundRobinAssignor策略的分配结果为:

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

可以看到RoundRobinAssignor策略保留了消费者C1和C2中原有的3个分区的分配:t2p0、t2p1和t2p2(针对结果集1)。而如果采用的是StickyAssignor策略,那么分配结果为:

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

可以看到StickyAssignor策略保留了消费者C1和C2中原有的5个分区的分配:t1p0、t1p1、t2p0、t2p1、t2p2。

从结果上看StickyAssignor策略比另外两者分配策略而言显得更加的优异。

猜你喜欢

转载自blog.csdn.net/qq_32727095/article/details/108133852