kafka重平衡

2,053 阅读7分钟

1. 什么是重平衡

重平衡(rebalance)本质上是一种协议,规定了一个消费组的消费者如何分配订阅主题的分区,同一个消费组不能有多个消费者同时消费同一个分区。

比如:某个消费组有20个消费者,订阅一个有100个分区的主题。一般情况下,kafka会平均给每个消费者分配5个分区,这个分配过程称为重平衡。

2. 什么情况会触发重平衡

重平衡的触发条件:

  • 消费组的成员发生变化,比如有新成员加入,有成员退出
  • 主题的分区数量发生变化,比如broker节点宕机,导致分区数减少
  • 订阅的主题数量发生变化,比如通过正则表达式进行订阅,有新的主题满足表达式,主题数量增多

3. 如何确定协调者

要进行重平衡,首先得要有一个broker节点担任消费组协调者,Group Coordinator。

确定协调者的步骤:

  • 第一步:确定消费组的位移信息写入到__consumer_offsets的哪个分区,具体计算公式:
    • 分区 = 消费组ID的哈希码 % groupMetadataTopicPartitionCount
    • groupMetadataTopicPartitionCount 由offsets.topic.num.partitions指定,默认是50
    • 也就是使用50个分区来存储偏移量信息
  • 第二步:知道具体分区后,该分区的leader福本所在的broker节点就是消费组的协调者

4. 协议

kafka提供了5个协议用来处理与消费组协调相关的问题:

  • Heartbeat请求:消费者需要定期给组协调者发送心跳,表明自己仍然存活
  • LeaveGroup请求:消费者主动告诉组协调者退出消费组
  • SyncGroup请求:消费组的Leader消费者把分区分配方案告知协调者,由协调者转发给组员
  • JoinGroup请求:新成员请求加入消费组
  • DescribeGroup请求:显示组的所有信息,包括组成员信息、分配方案、订阅信息等。通常该请求是给管理员使用

在重平衡过程中,主要用到前4种请求。

5. 重平衡过程

重平衡过程主要分为两步:

  • 第一步:请求加入消费组。所有成员都向消费组协调器发送JoinGroup请求,请求加入消费组。一旦所有成员都发送了 JoinGroup 请求,协调器从中选择一个消费者担任 Leader 角色,并把成员信息以及订阅信息发送给 Leader消费者
  • 第二步:同步分区分配方案。Leader消费者接收到消费组信息后,制定分区分配方案,即哪个消费者负责消费哪些主题的哪些分区。制定完方案后,Leader消费者会把方案封装在SyncGroup请求,发给消费组协调者。非Leader也会发送 SyncGroup 请求,只不过内容为空。协调者接收到分配方案后,把方案放在response中返回给消费者
image-20210927095307229
image-20210927095740165

6. 分区分配策略

kafka提供了三种分区分配的策略,分别是 RangeAssignor、RoundRobinAssignor 和 StickyAssignor。

6.1 RangeAssignor

RangeAssignor分配方法大致过程是,把每个主题的分区按照分区ID有序排列,然后对订阅这个主题的消费组的消费者按照字典排序。最后,尽量均衡的将分区分配给消费者。比如:现在某个主题有7个分区,消费组有3个消费者,那么分配结果是,第一个消费者分到3个分区,其他消费者分到2个分区。

image-20210927100707606

简单来说,分配原理是,将分区总数除以消费者总数得到一个跨度,然后将分区按照跨度进行平均分配,保证尽可能均匀分配给每个消费者。如果不能平均分配,字典值靠前的消费者会被多分配一个分区

这种分配方式导致的不良后果是,随着订阅主题数增多,字典值靠前的消费者分配到的分区数将会越来越多,与其他消费者分配的分区数差距越来越大,影响整体性能。

image-20210927102438778

6.2 RoundRobinAssignor

RoundRobinAssignor分配策略大致过程是,将所有主题的所有分区,以及消费组的消费者进行排序后,依次给每个消费者分配分区。类似,遍历所有分区,轮询的方式给消费者每次分配一个分区。如下图所示

image-20210927103432293

相对于RangeAssignor,在订阅多个主题的情况下,轮询分配的方式能够尽量均衡的给消费者分配分区,消费者之间分配到的分区数量差值不会超过1。

但是,如果消费组内消费者订阅的主题不一致的话,还是会出现分配不均匀的情况。比如:消费组内C0订阅主题1和主题2,C1仅订阅主题2,那么分配情况如下:

image-20210927103837264

6.3 StickyAssignor

无论是基于范围分配的方式,也还是基于轮询分配的方式,都没有考虑到上一次的分配结果。如果能够考虑这个因素,能够尽量少的调整分区分配的变动,可以节省很多开销。

StickyAssignor的具体实现比较复杂,我们直接看下分配的结果。

举个例子,当前情况是:

  • 有一个消费组,包含3个消费者
  • 有4个主题,每个主题都有2个分区
  • 所有的消费者都订阅了这4个主题

初始分配情况如下:

image-20210927105240546

此时消费者1宕机, 如果使用轮询方式重新分配的话,消费者分配到的分区都要进行调整。

image-20210927105616744

如果使用 Sticky 方式重新分配,只会对消费者1负责分区进行重新调整,最终达到均衡的目的。(红线是调整后的结果)

image-20210927110028484

与轮询方式相比,Sticky 分配方式尽可能减少原来分区的调整,并达到均衡分配的效果。

7. Rebalance Generation

Rebalance Generation,表示重平衡后主题分区与消费组的消费者之间映射关系的一个版本。主要作用是用来保护消费组,隔离无效偏移量提交。比如,某个消费者负载过重,或者处理时间超过心跳响应时间,被误认为是宕机,这时触发了重平衡。这个机器完成业务处理后,要提交偏移量到 broker,由于 generation号比最新的小1,不能提交偏移量。

每次消费组执行重平衡后, generation号 都会加1,表示消费者和分区的映射关系到了一个新版本。下面是一个示例,刚开始 generation号是1,然后某个消费者宕机,执行重平衡,generation号变成2.

image-20210927111242605

8. 重平衡带来的问题

在重平衡过程中,消费者是不能从kafka消费消息的,这就对系统性能造成影响。如果kafka节点较多,比如数百个,那么重平衡所需要的时间可能数分钟,甚至数小时,这段期间kafka几乎是不可用。所以,在实际环境中,我们要尽量避免重平衡的发生

9. 避免重平衡

我们知道,要触发重平衡有几种条件,分别是消费者发生变化、分区发生变化、订阅主题发生变化。无论是增减分区数,还是增加消费者数量,这都是主动控制。而消费者故障是最容易发生的,也是最常见引发重平衡的地方。

怎么避免消费者发生故障?

这也是没法避免的,我们的做的是保证不要让kafka错误判断消费者发生故障。这主要与三个参数有关系:

  • session.timeout.ms:控制心跳超时时间
  • heartbeat.interval.ms:控制发送心跳的频率,频率越高越不容易被误判,但是消耗更多资源
  • max.poll.intervel.ms:控制poll间隔。消费者poll数据后,进行业务处理,然后再次拉取数据。如果两次poll的间隔大于这个参数值,就会被提出消费者组,默认是5分钟

较为合理的配置是:

  • session.timeout.ms = 6000
  • heartbeat.interval.ms = 2000
  • max.poll.interval.ms = 消费者处理消息最长耗时+1分钟