kafka rebalance重平衡
1.rebalance概述
Rebalance 本质上是一种协议,规定了一个 Consumer Group 下的所有 Consumer 如何达成一致,来分配订阅 Topic 的每个分区。
假设某个组下有 20 个 consumer 实例,该组订阅了一个有着 100 个分区的 topic 。 正常情况下, Kafka 会为每个 consumer 平均分配 5 个分区。这个分配过程就被称为 rebalance 。 当 consumer 成功地执行 rebalance 后,组订阅 topic 的每个分区只会分配给组内的一个 consumer 实例。
Kafka 内置的一个全新的组协调协议(group coordination protocol) 。对于每个组而言, Kafka 的某个 broker 会被选举为组协调者(group coordinator)。coordinator 负责对组的状态进行管理,它的主要职责就是当新成员到达时促成组内所有成员达成新的分区分配方案,即 coordinator 负责对组执行 rebalance 操作。
2.rebalance触发条件
1)组成员数发生变更。比如有新的 Consumer 实例加入组或者离开组,抑或是有 Consumer 实例崩溃被“踢出”组。99% 都是这个原因
2)订阅主题数发生变更。主要基于正则表达式的订阅,当匹配正则表达式的新 topic 被创建时则会触发 rebalance。
3)订阅主题的分区数发生变更。
3.rebalance分区分配策略
分区分配策略决定了将topic中partition分配给consumer group中consumer实例的方式。
默认提供了 3 种分配策略,分别是 range、round-robin和sticky。通过 consumer 参数 partition.assignment. strategy 来进行配置。
range 分配策略的原理是按消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配,以保证分区尽可能平均的分配给所有的消费者。
假设 n = 分区数/消费者数量,m= 分区数%消费者数量,那么前m个消费者每个分配n+1个分区,后面的(消费者数量-m)个消费者每个分配n个分区。
round-robin 分配策略是将消费者组内所有主题的分区按照字典序排序,然后通过轮询的方式逐个将分区一次分配给每个消费者。
sticky 分配策略是从0.11.x版本开始引入的分配策略,它主要有两个目的:
1)分区的分配要尽可能均匀。
2)分区的分配尽可能与上次分配的保持相同。
当两者发生冲突时,第一个目标优于第二个目标。sticky具体实现要比上面两种要复杂的多。
我们以一个具体的例子来说明。
假设消费者组内有3个消费者(C0、C1、C2),他们都订阅了4个主题(t0、t1、t2、t3),并且每个主题有两个分区。也就是说,整个消费者组订阅了t0p0、t0p1、t1p0、t1p1、t2p0、t2p1、t3p0、t3p1 8个分区。最终的分配结果为
消费者C0:t0p0、t1p1、t3p0
消费者C1:t0p1、t2p0、t3p1
消费者C2:t1p0、t2p1
这看上去似乎与round robin分配策略相同,事实上并不是这样。假设此时C1脱离了消费者组,那么消费者组就会执行rebalance,进而消费分区会重新分配。如果采用round robin策略,那么此时的分配结果如下
消费者C0:t0p0、t1p0、t2p0、t3p0
消费者C2:t0p1、t1p1、t2p1、t3p1
如果采用sticky分配策略,那么分配结果为
消费者C0:t0p0、t1p1、t3p0、t2p0
消费者C2:t1p0、t2p1、t0p1、t3p1
可以看到分配结果中保留了上一次分配中对消费者C0和C2的所有的分配结果,并将原来的消费者C1的负担分配给了剩余的两个消费者C0和C1,最终C0和C2的分配还保持了平衡。
4.rebalance的弊端
1)Rebalance 影响 Consumer 端 TPS。(因为rebalance过程中,kafka会停止消费)
2)Rebalance 要完成需要比较久的时间。
3)Rebalance 效率不高,每次都要全部consumer参加。
5.如何解决99%的rebalance?
1)非必要 Rebalance 是因为未能及时发送心跳,导致 Consumer 被“踢出”Group 而引发的。
当 Consumer Group 完成 Rebalance 之后,每个 Consumer 实例都会定期地向 Coordinator 发送心跳请求,表明它还存活着。如果某个 Consumer 实例不能及时地发送这些心跳请求,Coordinator 就会认为该 Consumer 已经“死”了,从而将其从 Group 中移除,然后开启新一轮 Rebalance。
这个发送心跳的间隔在Consumer 端有个参数,叫 session.timeout.ms,默认值是 10 秒,即如果 Coordinator 在 10 秒之内没有收到 Group 下某 Consumer 实例的心跳,它就会认为这个 Consumer 实例已经挂了。session.timout.ms 决定了 Consumer 存活性的时间间隔。
除了这个参数,Consumer 还提供了一个允许你控制发送心跳请求频率的参数,就是 heartbeat.interval.ms。这个值设置得越小,Consumer 实例发送心跳请求的频率就越高。频繁地发送心跳请求会额外消耗带宽资源,但好处是能够更加快速地知晓当前是否开启 Rebalance,因为,目前 Coordinator 通知各个 Consumer 实例开启 Rebalance 的方法,就是将 REBALANCE_NEEDED 标志封装进心跳请求的响应体中。
Consumer 端还有一个参数,用于控制 Consumer 实际消费能力对 Rebalance 的影响,即 max.poll.interval.ms 参数,默认5min,Consumer 端应用程序两次调用 poll 方法的最大时间间隔,表示你的 Consumer 程序如果在 5 分钟之内无法消费完 poll 方法返回的消息,那么 Consumer 会主动发起“离开组”的请求,Coordinator 也会开启新一轮 Rebalance。
所以可以修改为以下经验推荐值:
- 设置 session.timeout.ms = 6s。
- 设置 heartbeat.interval.ms = 2s。
- 要保证 Consumer 实例在被判定为“dead”之前,能够发送至少 3 轮的心跳请求,即 session.timeout.ms >= 3 * heartbeat.interval.ms。
将 session.timeout.ms 设置成 6s 主要是为了让 Coordinator 能够更快地定位已经挂掉的 Consumer。
2)非必要 Rebalance 是 Consumer 消费时间过长导致的。所以可以估算消费这条消息后,处理的时间,设置max.poll.interval.ms大于等于这个时长多1min。
如果这些都设置好,还是出现rebalance,可以排查一下Consumer 端的 GC 表现,比如是否出现了频繁的 Full GC 导致的长时间停顿,从而引发了 Rebalance。
6.Rebalance流程
重平衡过程是如何通知到其他消费者实例的?答案就是,靠消费者端的心跳线程(Heartbeat Thread)。
当协调者决定开启新一轮重平衡后,它会将“REBALANCE_IN_PROGRESS”封装进心跳请求的响应中,发还给消费者实例。当消费者实例发现心跳响应中包含了“REBALANCE_IN_PROGRESS”,就能立马知道重平衡又开始了,这就是重平衡的通知机制。
消费者组状态机
一个消费者组最开始是 Empty 状态,当重平衡过程开启后,它会被置于 PreparingRebalance 状态等待成员加入,之后变更到 CompletingRebalance 状态等待分配方案,最后流转到 Stable 状态完成重平衡。
当有新成员加入或已有成员退出时,消费者组的状态从 Stable 直接跳到 PreparingRebalance 状态,此时,所有现存成员就必须重新申请加入组。当所有成员都退出组后,消费者组状态变更为 Empty。Kafka 定期自动删除过期位移的条件就是,组要处于 Empty 状态。因此,如果你的消费者组停掉了很长时间(超过 7 天),那么 Kafka 很可能就把该组的位移数据删除了。我相信,你在 Kafka 的日志中一定经常看到下面这个输出:
Removed ✘✘✘ expired offsets in ✘✘✘ milliseconds.
这就是 Kafka 在尝试定期删除过期位移。现在你知道了,只有 Empty 状态下的组,才会执行过期位移删除的操作。