背景
年初的时候用搭建过一个数据处理系统,mq用的kafka,当时对kafka的分区策略不明确,用的默认策略,即RangeAssigor,但我并不知道具体的分区消费逻辑。这几天趁着架构组向下推广kafka, 我研究了具体的分区原理。
我有个疑问
最新的kafka有三种分区策略,分别是RangeAssigor、RoundRobinAssignor、StickyAssignor,默认的策略是RangeAssigor,客户端可以指定使用某种策略,那这三种策略的原理、应用场景是什么?
主题及分区例子
A组: 两个主题T1 、 T2 ,每个主题配备四个分区,分别为P1 、P2 、P3 、 P4
B组: 两个主题T1、T2, 每个主题配备三个分区,分别为P1 、P2 、P3
C组: 一个主题T1,每个主题配备四个分区,分别为P1、P2、P3、P4
D组: 三个主题T1、T2、T3, 每个主题分别有 1 、 2、 3个分区
E组:四个主题T1、T2、T3、T4,每个主题分别有俩分区
由于在kafka中,一个消费组里的消费者可以订阅多个topic,每个topic会包含多个分区,但默认情况下一个分区只能被一个消费组下面的一个消费者消费,并且如果消费者没有订阅某个主题,那么该消费者不会分配到这个主题下的分区。
那么为什么我们要分区? 分区对于 Kafka 集群的好处是:实现负载均衡。分区对于消费者来说,可以提高并发度,提高效率。
所以我们把主题与分区进行排列组合一下,即:
A组: T1P1、T1P2 、 T1P3、 T1P4、 T2P1、 T2P2、 T2P3、 T2P4八种
B组: T1P1、T1P2 、 T1P3、 T2P1、 T2P2、 T2P3 六种
C组: T1P1、 T1P2、 T1P3 、T1P4 四种
D组:T1P1 、T2P1、 T2P2、T3P1 、 T3P2、 T3P3 六种
E组:T1P1、T1P2、T2P1、T2P2、T3P1、T3P2、T4P1、T4P2
分析及举例
RangeAssigor分区策略:RangeAssigor策略是对每个topic单独进行分配的,原理是按照消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配,以保证分区尽可能均匀地分配给所有的消费者。
同一消费组订阅同一主题
- 比如当前消费组里有一个消费者C1,那么拿C组4个分区(T1P1、 T1P2、 T1P3 、T1P4 )来举例,毋庸置疑,所有分区的数据都需要它来消费,也就是说消费者C1,对应的分区为
- C1 => T1P1、 T1P2、 T1P3 、T1P4
- C1 => T1P1、 T1P2、 T1P3 、T1P4
- 如果我们再往消费组里加一个消费者C2,那么分区会如何分配呢?此时有两个消费者,四个分区, RangeAssigor策略会将分区先按照id顺序排序,然后将消费者按字典顺序排序,尽量均匀的分配给消费组里的消费者,即
- C1 => T1P1、 T1P2
- C2=> T1P3 、T1P4
- 上述的分区是偶数的,遇到可以被整除的消费者,每个消费者获得的可消费分区数量都是均匀的,加入我们的C组中只有三个分区呢?此时有两个消费者,三个分区,那该如何分配? 此时C组排列组合为 T1P1、 T1P2、 T1P3
- C1=> T1P1、 T1P2
- C2=> T1P4
- 那如果我们的消费者个数大于分区数,又会怎么分配呢,比如现在消费组里有5个消费者C1、C2、C3、C4、C5,分区排列是C组,分区个数X < 消费者个数Y 的情况,此时就会有 Y-X个消费者被闲置,及字典顺序靠后的消费者不会分配到分区
- C1 => T1
- C2 => T2
- C3 => T3
- C4 => T4
- C5 闲置
同一消费组消费订阅不同主题
- 拿A组2个主题8个分区(T1P1、T1P2 、 T1P3、 T1P4、 T2P1、 T2P2、 T2P3、 T2P4)举例,有两个消费者C1、C2分别订阅了这俩主题,我们对分区、消费者排好序之后开始分配,由于rangeassigor策略是针对每个topic单独分配的,所以原理是
- 先从把主题T1的三个分区分配给消费者,即
- C1 => T1P1、T1P2
- C2 => T1P3、 T1P4
- 然后再把主题T2的三个分区分配给消费者, 即
- C1 => T1P1、T1P2、 T2P1、 T2P2
- C2=> T1P3 、T1P4、 T2P3、 T2P4
- 先从把主题T1的三个分区分配给消费者,即
此时看,由于每个主题的分区个数都可以被同一消费组的消费者个数整除,所以此时看着分配的还听均匀哈,那如果不能被整除的话,由会如何分配呢?
- 拿B组2个主题6个分区(T1P1、T1P2 、 T1P3、 T2P1、 T2P2、 T2P3)举例,有2个消费之C1 、C2 分别订阅了这俩主题,按照分配策略,每个主题分区不被消费者个数整除的情况,字典顺序靠前的消费者就会多承担分区的策略,分配结果如下
- 先从把主题T1的三个分区分配给消费者,即
- C1 => T1P1、T1P2
- C2 => T1P3
- 然后再把主题T2的三个分区分配给消费者, 即
- C1 => T1P1、T1P2、T2P1、 T2P2
- C2 => T1P3、 T2P3
- 先从把主题T1的三个分区分配给消费者,即
此时再看,消费者承担的分区就不均匀了,kafka里有很多个这样的主题会发生什么呢?答案是消费者C1承担的分区过多, 负载不均衡,会造成挤压,消费不及时,可见这种消费有问题。
RoundRobinAssignor分区策略:RoundRobinAssignor策略原理是将所有分区都按照字典顺序排序,然后通过轮询的方式均匀的分配给消费组内的每个消费者,我们可以拿此策略与nginx的roundrobin轮询策略类别,原理上是一致的。
由于是将所有主题与分区的排列组合按照字典顺序排序,所以我们无需考虑统一消费组内订阅几个主题的情况
- 我们拿B组的的6个分区(T1P1、T1P2 、 T1P3、 T2P1、 T2P2、 T2P3)来举例,消费组内有2个消费者C1、C2,此时分区个数能被消费者个数整除,按照轮询的方式分配结果如下
- C1 => T1P1 、 T1P3、 T2P2
- C2 => T1P2、 T2P1、 T2P3
我们可以看到,RoundRobinAssignor分区策略解决了RangeAssigor的问题,遇到分区个数不能被消费者个数争取的情况,轮询策略分配的会更加均匀,从而达到避免过载问题。、
- 假如A组只有7个分区(T1P1、T1P2 、 T1P3、 T1P4、 T2P1、 T2P2、 T2P3),没有T2P4分区时,此时分区个数不能被消费者个数整除,轮询分配策略又会如何分配呢?答案是,给消费者一个个轮,少的就少吧,就是这么随性
- C1 => T1P1 、 T1P3、 T2P1、 T2P3
- C2 => T1P2、 T1P4、 T2P2
这样分配看起来依旧不错,无伤大雅
- 那么我们再举个伤大雅的例子,哈哈哈哈,上我们的D组,3个主题6个分区(T1P1 、T2P1、 T2P2、T3P1 、 T3P2、 T3P3),请仔细看下D组,每个主题对应的分区不同哈,消费组里配备3个消费者C1、C2、C3,C1订阅T1,C2订阅T1、T2,C3订阅T1、T2、T3,分区个数可以被消费者个数整除,那么分配结果如下
- C1 => T1P1
- C2 =>T2P1
- C3 =>T2P2、T3P1、T3P2、T3P3
- C1订阅T1,那么它得到T1P1,没有问题
- C2订阅了T2 , 按照roundRobin策略T2P1被分配给C2, 也没问题
- C3被分配了T2P2,原理同上,没有问题
- 接下来就是按照roundRobin策略开始分配T3的分区,但是我们发现,C1、C2并没有订阅T3,那么T3的所有分区都由C3承担
那么你看出问题了吗,此种情况下,分区又不均匀了,C3挂上的分区过多,负载变高,一般情况下咱们用不到同一个消费组如此订阅主题,这种比较高级。
StickyAssignor分区策略:Kafka从0.11.x版本开始引入这种分配策略,属于对roundRobin的一种优化,他有俩目的,一个是为了更均匀的分配分区,另一个就是分区的分配尽可能的与上次分配的保持相同,这点可能这么看不理解,下面我会具体举例给解释一下
- 我们用E组的4个主题,每个主题2个分区,共8个分区(T1P1、T1P2、T2P1、T2P2、T3P1、T3P2、T4P1、T4P2),消费组里配置3个消费者C1、C2、C3,如果使用使用StickyAssignor分区策略,分配结果如下
- C1=>T1P1、T2P2、T4P1
- C2=>T1P2、T3P1、T4P2
- C3=>T2P1、T3P2
有没有很感动?这不就是roundRobin策略吗?你看结果都一样,你是不是耍我?别急,我说了这是roundRobin的一种优化嘛,如果此时C2突然肚子痛挂了呢?会引起组内rebalance,重新分配分区,分配结果如下
-
- C1=>T1P1、T2P2、T4P1、T3P1
- C3=>T2P1、T3P2、T1P2、T4P2
此时还不直观,我们再把roundRobin策略下C2挂了的情况触发rebalance后分配结果列一下
-
- C1=>T1P1、T2P1、T3P1、T4P1
- C2=>T1P2、T2P2、T3P2、T4P2
对比StickyAssignor、roundRobinAssignor两种策略rebalance后分配结果,你发现了什么?roundRobinAssignor实际上是在C2挂了之后,重新将8个分区按规则均匀分配到C1、C3上,但是StickyAssignor缺没有重新分配,而是在原有C1、C3的已有分区的基础上,继续分配C2的分区,并且还实现了均匀分配,保证负载均衡,现在觉得呢,是不是StickyAssignor策略更好一些?你可能要问,就为了个避免全部重新分配?这里牵扯出rebalance后,消费者重新消费新分区的时耗问题,尽可能少的分配,才会更快的拉起服务。
结论
目前咱们引入的kafka, 分配策略还处在默认策略即RangeAssigor, 这个分业务,目前看,咱们的业务几乎都是一个消费组订阅同一个主题,并且架构组提供的扩展也是,同一消费组仅支持消费一个主题,如果需要消费多个主题,那得改造一下。理论上默认策略够用,也不会引起负载问题。而如果我们有一个消费组订阅不同主题的情况,我建议直接使用StickyAssignor策略,因为他在消费者或者分区挂了时,rebalance更均匀,更友好。最后策略可以由客户端指定,不是说咱放着好的策略不用,这得根据场景选。
这里附上kafka官方文档,大家有兴许可以继续深入 https://kafka.apache.org/24/javadoc/org/apache/kafka/clients/consumer/