六、kafka生产者和消费者的分区分配策略

kafka生产者和消费者的分区分配策略

一、producer的分区分配策略

我们向topic发送消息的时候是要把messages封装成一个ProducerRecord对象的,源码如下:
在这里插入图片描述
可以看到再new一个ProducerRecord对象时可分为三种情况:
1.指明 partition 的情况下,直接将指明的值直接作为 partiton 值;
2.没有指明 partition 值但有 key 的情况下,将 key 的 hash 值与 topic 的 partition 数进行取余得到 partition 值;(具体实现可参考:默认分区器org.apache.kafka.clients.producer.internals.DefaultPartitioner中的 partition() 方法)
3.既没有 partition 值又没有 key 值的情况下,第一次调用时随机生成一个整数(后面每次调用在这个整数上自增),将这个值与 topic 可用的 partition 总数取余得到 partition值,也就是常说的 round-robin 算法。这也是默认的分区分配策略,能够保证负载均衡

二、consumer的分区分配策略

说一个名词:Kafka再平衡机制。

所谓的再平衡,指的是在kafka consumer所订阅的topic发生变化时发生的一种分区重分配机制。

一般有三种情况会触发consumer的分区分配策略(再平衡机制)

  • consumer group中的新增或删除某个consumer,导致其所消费的分区需要分配到组内其他的consumer上;
  • consumer订阅的topic发生变化,比如订阅的topic采用的是正则表达式的形式,如test-*此时如果有一个新建了一个topic test-user,那么这个topic的所有分区也是会自动分配给当前的consumer的,此时就会发生再平衡;
  • consumer所订阅的topic发生了新增分区的行为,那么新增的分区就会分配给当前的consumer,此时就会触发再平衡。

consumer的分区分配策略主要有三种:Round Robin,Range和Sticky,默认使用的是Range。这三种分配策略的主要区别在于:

  • Round Robin:会采用轮询的方式将当前所有的分区依次分配给所有的consumer;
  • Range:首先会计算每个consumer可以消费的分区个数,然后按照顺序将指定个数范围的分区分配给各个consumer;
  • Sticky:这种分区策略是最新版本中新增的一种策略,其主要实现了两个目的:
  • 将现有的分区尽可能均衡的分配给各个consumer,存在此目的的原因在于Round Robin和Range分配策略实际上都会导致某几个consumer承载过多的分区,从而导致消费压力不均衡;
  • 如果发生再平衡,那么重新分配之后在前一点的基础上会尽力保证当前未宕机的consumer所消费的分区不会被分配给其他的consumer上;
    在代码中:org.apache.kafka.clients.consumer.internals.AbstractPartitionAssignor这个类默认有3种实现方式,如果要自定义分配策略的话,只需要继承AbstractPartitionAssignor这个类
    在这里插入图片描述

2.1、Round Robin策略(轮询策略)

Round Robin (轮询)策略针对是一个消费者组,根据组来分的。
roundronbin分配策略的具体实现是org.apache.kafka.clients.consumer.RoundRobinAssignor
意思就是一个group中的两个consumer同时订阅了两个topic,这两个topic都有3个分区,其中一个consumer被分配到了t0p0, t0p2, t1p1,另一个consumer被分配到了t0p1, t1p0, t1p2,那么为什么会这样分配呢?
这是因为轮询分配策略是基于所有可用的消费者和所有可用的分区的,与range策略最大的不同就是它不再局限于某个主题,如果所有的消费者实例的订阅都是相同的,那么这样最好了,可用统一分配,均衡分配。
假设,组中每个消费者订阅的主题不一样,分配过程仍然以轮询的方式考虑每个消费者实例,但是如果没有订阅主题,则跳过实例。当然,这样的话分配肯定不均衡。

什么意思呢?也就是说,消费者组是一个逻辑概念,同组意味着同一时刻分区只能被一个消费者实例消费,换句话说,同组意味着一个分区只能分配给组中的一个消费者。事实上,同组也可以不同订阅,这就是说虽然属于同一个组,但是它们订阅的主题可以是不一样的。

看个例子:

这里我们首先假设有三个topic:t0、t1和t2,这三个topic拥有的分区数分别为1、2和3,那么总共有六个分区,这六个分区分别为:t0-0、t1-0、t1-1、t2-0、t2-1和t2-2。这里假设我们有三个consumer:C0、C1和C2,它们订阅情况为:C0订阅t0,C1订阅t0和t1,C2订阅t0、t1和t2。那么这些分区的分配步骤如下:

  • 首先将所有的partition和consumer按照字典序进行排序,所谓的字典序,就是按照其名称的字符串顺序,那么上面的六个分区和三个consumer排序之后分别为:

在这里插入图片描述

  • 然后依次以按顺序轮询的方式将这六个分区分配给三个consumer,如果当前consumer没有订阅当前分区所在的topic,则轮询的判断下一个consumer:

  • 尝试将t0-0分配给C0,由于C0订阅了t0,因而可以分配成功;

  • 尝试将t1-0分配给C1,由于C1订阅了t1,因而可以分配成功;

  • 尝试将t1-1分配给C2,由于C2订阅了t1,因而可以分配成功;

  • 尝试将t2-0分配给C0,由于C0没有订阅t2,因而会轮询下一个consumer;

  • 尝试将t2-0分配给C1,由于C1没有订阅t2,因而会轮询下一个consumer;

  • 尝试将t2-0分配给C2,由于C2订阅了t2,因而可以分配成功;

  • 同理由于t2-1和t2-2所在的topic都没有被C0和C1所订阅,因而都不会分配成功,最终都会分配给C2。

  • 按照上述的步骤将所有的分区都分配完毕之后,最终分区的订阅情况如下:
    在这里插入图片描述

从上面的步骤分析可以看出,轮询的策略就是简单的将所有的partition和consumer按照字典序进行排序之后,然后依次将partition分配给各个consumer,如果当前的consumer没有订阅当前的partition,那么就会轮询下一个consumer,直至最终将所有的分区都分配完毕。但是从上面的分配结果可以看出,(缺点:)轮询的方式在这种情况下会导致每个consumer所承载的分区数量不一致,从而导致各个consumer压力不均一。

2.2、Range策略

Range策略是按照topic依次进行分配的(针对的是主题topic)。
range策略对应的实现类org.apache.kafka.clients.consumer.RangeAssignor ,同时这是默认的分配策略。

可以通过consumer配置 partition.assignment.strategy 参数来指定分配策略,它的值是类的全路径,是一个数组。

对于每个主题,我们以数字顺序排列可用分区,以字典顺序排列消费者。然后,将分区数量除以消费者总数,以确定分配给每个消费者的分区数量。如果没有平均划分(有余数),那么最初的几个消费者将有一个额外的分区。
简而言之,就是:

1、range分配策略针对的是主题(也就是说,这里所说的分区指的某个主题的分区,消费者指的是订阅这个主题的消费者组中的所有消费者)
2、首先,将分区按数字顺序排行序,消费者按消费者名称的字典序排好序,然后,用分区总数除以消费者总数。如果能够除尽,则皆大欢喜,平均分配
3、若除不尽,则位于排序前面的消费者将多负责一个分区。

举个例子:

我们假设有两个consumer:C0和C1,两个topic:t0和t1,这两个topic分别都有三个分区,那么总共的分区有六个:t0-0、t0-1、t0-2、t1-0、t1-1和t1-2。那么Range分配策略将会按照如下步骤进行分区的分配:

我们以t0进行讲解,其首先会获取 t0 的所有分区:t0-0、t0-1和t0-2,以及所有订阅了该topic的consumer:C0和C1,并且会将这些分区和consumer按照字典序进行排序。
然后按照平均分配的方式计算每个consumer会得到多少个分区,如果没有除尽,则会将多出来的分区依次计算到前面几个consumer。比如这里是三个分区和两个consumer,那么每个consumer至少会得到1个分区,而3除以2后还余1,那么就会将多余的部分依次算到前面几个consumer,也就是这里的1会分配给第一个consumer,总结来说,那么C0将会从第0个分区开始,分配2个分区,而C1将会从第2个分区开始,分配1个分区。
同理,按照上面的步骤依次进行后面的topic的分配。
最终上面六个分区的分配情况如下:
在这里插入图片描述
(缺点:)可以看到,如果按照Range分区方式进行分配,其本质上是依次遍历每个topic,然后将这些topic的分区按照其所订阅的consumer数量进行平均的范围分配。这种方式从计算原理上就会导致排序在前面的consumer分配到更多的分区,从而导致各个consumer的压力不均衡。

2.3、StickyAssignor策略

Sticky策略是Kafka0.11.x版本新增的策略,顾名思义,这种策略会保证再分配时已经分配过的分区尽量保证其能够继续由当前正在消费的consumer继续消费,当然,前提是每个consumer所分配的分区数量都大致相同,这样能够保证每个consumer消费压力比较均衡。

它主要有两个目的:

分区的分配要尽可能的均匀;
分区的分配尽可能的与上次分配的保持相同。
当两者发生冲突时,第一个目标优先于第二个目标。

关于这种分配方式的分配策略,我们分两种情况进行讲解,即初始状态的分配和某个consumer宕机时的分配情况。

2.3.1、初始分配
初始状态分配的特点是,所有的分区都还未分配到任意一个consumer上。这里我们假设有三个consumer:C0、C1和C2,三个topic:t0、t1和t2,这三个topic分别有1、2和3个分区,那么总共的分区为:t0-0、t1-0、t1-1、t2-0、t2-1和t2-2。关于订阅情况,这里C0订阅了t0,C1订阅了t0和1,C2则订阅了t0、t1和t2。这里的分区分配规则如下:

  • 首先将所有的分区进行排序,排序方式为:首先按照当前分区所分配的consumer数量从低到高进行排序,如果consumer数量相同,则按照分区的字典序进行排序。这里六个分区由于所在的topic的订阅情况各不相同,因而其排序结果如下:
    在这里插入图片描述
  • 然后将所有的consumer进行排序,其排序方式为:首先按照当前consumer已经分配的分区数量有小到大排序,如果两个consumer分配的分区数量相同,则会按照其名称的字典序进行排序。由于初始时,这三个consumer都没有分配任何分区,因而其排序结果即为其按照字典序进行排序的结果:
    在这里插入图片描述
  • 然后将各个分区依次遍历分配给各个consumer,首先需要注意的是,这里的遍历并不是C0分配完了再分配给C1,而是每次分配分区的时候都整个的对所有的consumer从头开始遍历分配,如果当前consumer没有订阅当前分区,则会遍历下一个consumer。然后需要注意的是,在整个分配的过程中,各个consumer所分配的分区数是动态变化的,而这种变化是会体现在各个consumer的排序上的,比如初始时C0是排在第一个的,此时如果分配了一个分区给C0,那么C0就会排到最后,因为其拥有的分区数是最多的。上面的六个分区整体的分配流程如下:
  • 首先将t2-0尝试分配给C0,由于C0没有订阅t2,因而分配不成功,继续轮询下一个consumer;
  • 然后将t2-0尝试分配给C1,由于C1没有订阅t2,因而分配不成功,继续轮询下一个consumer;
  • 接着将t2-0尝试分配给C2,由于C2订阅了t2,因而分配成功,此时由于C2分配的分区数发生变化,各个consumer变更后的排序结果为:
    在这里插入图片描述
  • 接下来的t2-1和t2-2,由于也只有C2订阅了t2,因而其最终还是会分配给C2,最终在t2-0、t2-1和t2-2分配完之后,各个consumer的排序以及其分区分配情况如下:
    在这里插入图片描述
  • 接着继续分配t1-0,首先尝试将其分配给C0,由于C0没有订阅t1,因而分配不成功,继续轮询下一个consumer;
  • 然后尝试将t1-0分配给C1,由于C1订阅了t1,因而分配成功,此时各个consumer以及其分配的分区情况如下:
    在这里插入图片描述
  • 同理,接下来会分配t1-1,虽然C1和C2都订阅了t1,但是由于C1排在C2前面,因而该分区会分配给C1,即:
    在这里插入图片描述
  • 最后,尝试将t0-0分配给C0,由于C0订阅了t0,因而分配成功,最终的分配结果为:
    在这里插入图片描述
    上面的分配过程中,需要始终注意的是,虽然示例中的consumer顺序始终没有变化,但这是由于各个分区分配之后正好每个consumer所分配的分区数量的排序结果与初始状态一致。这里读者也可以比较一下这种分配方式与前面讲解的Round Robin进行对比,可以很明显的发现,Sticky重分配策略分配得更加均匀一些。

2.3.2、模拟consumer宕机

由于前一个示例中最终的分区分配方式模拟宕机的情形比较简单,因而我们使用另一种订阅策略。这里我们的示例的consumer有三个:C0、C1和C2,topic有四个:t0、t1、t2和t3,每个topic都有两个分区,那么总的分区有:t0-0、t0-1、t1-0、t1-1、t2-0、t2-1、t3-0和t3-1。这里的订阅情况为三个consumer订阅所有的主题,那么如果按照Sticky的分区分配策略,初始状态时,分配情况如下,读者可以按照前一示例讲解的方式进行推算:
在这里插入图片描述
这里我们假设在消费的过程中,C1发生了宕机,此时就会发生再平衡,而根据Sticky策略,其再分配步骤如下:

  • 首先会将宕机之后未分配的分区进行排序,排序方式为:首先按照分区所拥有的consumer数量从低到高进行排序,如果consumer数量相同,则按照分区的字典序进行排序。这里需要注意的是,由于只有C1宕机,因而未分配的分区为:t0-1、t2-0和t3-1,排序之后的结果为:
    在这里插入图片描述

  • 然后将所有的consumer进行排序,排序方式为:首先将consumer按照其所拥有的consumer数量从小到大排序,如果数量相同,则按照consumer名称的字典序进行排序,排序结果如下:
    在这里插入图片描述

  • 接着依次遍历各个分区,将其分配给各个consumer,需要注意的是,在分配的过程中,consumer所分配的分区数量是在变化的,而这种变化是会反应在consumer的排序上的:

  • 首先尝试将t0-1分配给C2,由于C2订阅了t0,因而可以分配成功,此时consumer排序和分区分配情况如下,需要注意的是,虽然分配之后,C2和C0的分区数量相同,但是由于按照字典序,C0在C2前面,因而排序情况还是会发生变化:
    在这里插入图片描述

  • 然后尝试将t2-0分配给C0,由于C0订阅了t2,因而分配可以成功,此时consumer排序和分区分配情况如下:
    在这里插入图片描述

  • 最后尝试分配t3-1给C2,由于C2订阅了t3,因而分配可以成功,此时consumer排序与分区分配情况如下:
    在这里插入图片描述
    在上面的分区分配过程中,我们可以看到,由于分区的不断分配,各个consumer所拥有的分区数量也在不断变化,因而其排序情况也在变化,但是最终可以看到,各个分区是均匀的分配到各个consumer的,并且还保证了当前consumer已经消费的分区是不会分配到其他的consumer上的。

猜你喜欢

转载自blog.csdn.net/qq_37200262/article/details/125209522