kafka的重平衡机制

Kafka中的分区(Partition)是物理上的概念,每个分区对应一个日志文件。为了实现扩展性,一个Topic可以包含多个分区,每个分区有一个 leader 作为主节点,可能还有多个 follower 作为从节点。对于同一个分区,其 leader 和 follower 是固定的。

leader 负责对分区的数据进行读写操作。follower 从 leader 中复制数据。如果 leader 失效,则从 follower 中选举新的 leader。这保证了分区的数据高可用。

为了实现负载均衡,Kafka 采用了分区重新分配的机制,即重平衡(Rebalance)。

什么是重平衡

在Kafka中,消费者组中的消费者通过心跳来保持与组协调器的连接。当消费者加入或离开组时,或当组订阅的主题或分区发生变化时,组协调器将触发重平衡。重平衡的目的是重新分配主题分区,以确保每个消费者都有相同数量的负载。也就是说重平衡就是让一个消费者组下所有的Consumer实例就如何消费订阅主题的所有分区达成共识的过程。在Rebalance过程中,所有Consumer实例共同参与,在Coordinator组件的帮助下,完成订阅主题分区的分配。但是,在整个过程中,所有实例都不能消费任何消息,因此它对Consumer的TPS影响很大。

其中Coordinator是kafka一个非常重要的组件,主要负责协调和管理Consumer Group的状态。包括管理Consumer重平衡。

什么情况下会重平衡

Kafka会在以下几种情况下触发重平衡:

  1. 集群 Broker 数量发生变化时。比如新增或关闭了 Broker,都会导致原有的分区和 Broker 的对应关系被打破,无法保持负载均衡,这就需要触发重平衡。

  2. 某个 Topic 的分区数发生了变化时。增加或减少了 Topic 的分区数都会直接影响到这个 Topic 的分区到 Broker 的映射关系。这时也需要重平衡来获取新的均衡的分区分布状态。

  3. 消费组内的消费者数量发生变化时。消费组是 Kafka 分区分配和消费的最小单位。如果一个消费组内的消费者数量增加减少了,那么该消费组内的分区到消费者的对应关系就需要重新计算。重平衡会生成一个新的均衡的分区分配方案。

  4. 消费者的订阅关系发生变更时。如果一个消费者新加入或者取消了某些 Topic 的订阅,由于其订阅关系改变,需要重新计算分区的分配方案,这时也需要触发重平衡。

  5. Broker 或消费者的配置参数发生变动时。配置参数变更可能会直接影响分区分配策略,所以这时也需要触发重平衡来生成新的分配方案。

  6. 集群 Controller 发生变化时。Controller 节点故障时会重新选举出新的 Controller,这时也需要重平衡以确保节点之间的数据和状态一致。

其中消费组内的消费者数量发生变化时是最常见触发重平衡的情况

重平衡过程

Kafka重平衡(Rebalance)的详细过程如下:

  1. 触发重平衡

    当Kafka集群发生变化时会触发重平衡,比如增加或删除主题分区、Broker加入或离开集群、消费组内消费者数量变化等。

  1. 准备重平衡

    Leader将分区和replica的信息发送给Controller。Controller根据分区分配策略准备新的分配方案。

  1. 重新分配分区

    Controller将重平衡计划下发给相关的Broker。这些Broker会停止当前分区的服务,从新Leader同步相应的分区数据。

  1. Consumer重新定位

    消费者向Group Coordinator注册,Coordinator根据分区分配方案指示每个消费者停止当前订阅,提交offset,然后重新订阅新分配的分区。

  1. 遗留数据处理

    对于一些遗留数据,Broker会将它们复制到新的负责处理该分区的Broker中。

  1. 稳定运行

    经过一段时间后,Broker完成数据迁移,Consumer定位到正确的分区,集群运行会恢复到稳定状态。

  1. 完成

    Controller检测状态,确保所有新的分区完成了重平衡,正常提供服务。至此整个重平衡过程完成。

重平衡的弊端

  1. Rebalance影响Consumer端TPS。在Rebalance期间,Consumer会停下手头的事情,什么也干不了。全部的消费实例都将停止。

  2. Rebalance很慢。如果你的Group下成员很多,就一定会有这样的痛点。如果Group下有几百个Consumer实例,Rebalance一次甚至要几十分钟。在这种场景下,Consumer Group的Rebalance已经完全失控了。

  3. Rebalance效率不高。当前Kafka的设计机制决定了每次Rebalance时,Group下的所有成员都要参与进来,而且通常不会考虑局部性原理,但局部性原理对提升系统性能是特别重要的。

关于以上这三个方面的弊端,举个简单的例子。比如一个Group下有10个成员,每个成员平均消费5个分区。假设现在有一个成员退出了,此时就需要开启新一轮的Rebalance,把这个成员之前负责的5个分区“转移”给其他成员。显然,比较好的做法是维持当前9个成员消费分区的方案不变,然后将5个分区随机分配给这9个成员,这样能最大限度地减少Rebalance对剩余Consumer成员的冲击。

遗憾的是,目前Kafka并不是这样设计的。在默认情况下,每次Rebalance时,之前的分配方案都不会被保留,当Rebalance开始时,Group会打散这50个分区(10个成员 * 5个分区),由当前存活的9个成员重新分配它们。显然这不是效率很高的做法。基于这个原因,社区于0.11.0.0版本推出了StickyAssignor,即有粘性的分区分配策略。所谓的有粘性,是指每次Rebalance时,该策略会尽可能地保留之前的分配方案,尽量实现分区分配的最小变动。不过有些遗憾的是,这个策略目前还有一些bug,而且需要升级到0.11.0.0才能使用,因此在实际生产环境中用得还不是很多。

如何避免重平衡

在真实的业务场景中,很多Rebalance都是计划外的或者说是不必要的。我们应用的TPS大多是被这类Rebalance拖慢的,因此避免这类Rebalance就显得很有必要了,前面提到消费组内的消费者数量发生变化时是最常见触发重平衡的情况,那么该如何避免呢

当Consumer增加的时候,也就是启动一个配置有相同group.id值的Consumer程序时,实际上就向这个Group添加了一个新的Consumer实例。这种的重平衡是无法避免的,也是理所应当的。关键是在某些情况下,Consumer实例会被Coordinator错误地认为“已停止”从而被“踢出”Group。如果是这个原因导致的Rebalance,那么就应该尽可能的避免。

当Consumer Group完成Rebalance之后,每个Consumer实例都会定期地向Coordinator发送心跳请求,表明它还存活着。如果某个Consumer实例不能及时地发送这些心跳请求,Coordinator就会认为该Consumer已经“死”了,从而将其从Group中移除,然后开启新一轮Rebalance。Consumer端有个参数,叫session.timeout.ms,就是被用来表征此事的。该参数的默认值是10秒,即如果Coordinator在10秒之内没有收到Group下某Consumer实例的心跳,它就会认为这个Consumer实例已经挂了。可以这么说,session.timeout.ms决定了Consumer存活性的时间间隔。

除了这个参数,Consumer还提供了一个允许你控制发送心跳请求频率的参数,就是heartbeat.interval.ms。这个值设置得越小,Consumer实例发送心跳请求的频率就越高。频繁地发送心跳请求会额外消耗带宽资源,但好处是能够更加快速地知晓当前是否开启Rebalance,因为,目前Coordinator通知各个Consumer实例开启Rebalance的方法,就是将REBALANCE_NEEDED标志封装进心跳请求的响应体中。

除了以上两个参数,Consumer端还有一个参数,用于控制Consumer实际消费能力对Rebalance的影响,即max.poll.interval.ms参数。它限定了Consumer端应用程序两次调用poll方法的最大时间间隔。它的默认值是5分钟,表示你的Consumer程序如果在5分钟之内无法消费完poll方法返回的消息,那么Consumer会主动发起“离开组”的请求,Coordinator也会开启新一轮Rebalance。

综上,给出如下建议配置

session.timeout.ms = 6s
heartbeat.interval.ms = 2s

其中要保证Consumer实例在被判定为“死了”之前,能够发送至少3轮的心跳请求,即session.timeout.ms >= 3 * heartbeat.interval.ms。而Consumer消费时间过长导致的Rebalance,直接和参数max.poll.interval.ms有关,可以将参数值设置得大一点,比你的下游最大处理时间稍长一点。总之,你要为你的业务处理逻辑留下充足的时间。这样,Consumer就不会因为处理这些消息的时间太长而引发Rebalance了。

如果你按照上面的推荐数值恰当地设置了这几个参数,却发现还是出现了Rebalance,那么可能需要排查一下Consumer端的GC表现,比如是否出现了频繁的Full GC导致的长时间停顿,从而引发了Rebalance。这也是较为常见非预期Rebalance了。

最后

kafka的重平衡有实现负载均衡、提高可用性等诸多有点,但是也有带来各种性能上的问题,同时有关它的bug也是此起彼伏,因此了解重平衡是至关重要的。