1.Kafka的重平衡机制
在集群模式下,一定会涉及到消费者节点数或Broker分区数的变化,如:
- 消费者加入/离开组(如扩容、宕机)
- 订阅 Topic 分区变化(如分区数增加)
- 心跳超时(默认 session.timeout.ms=45s)等
Kafka 的重平衡机制是指在消费者组中新增或删除消费者时,Kafka 集群会重新分配主题分区给各个消费者,以保证每个消费者消费的分区数量尽可能均衡。
重平衡机制的目的是实现消费者的负载均衡和高可用性,以确保每个消费者都能够按照预期的方式消费到消息
虽然kafka没有像RocketMq那样有NameServer的注册中心去维护Broker、消费者、发送者之间的联系,但是它可以通过组织协调器 (Group Coordinator) 管理消费者组的 Broker 节点。
如果它崩渍或者发生故障Kafka 需要重新选举新的 Group Coordinator ,并进行重平衡。
而且当消费者组中的 Leader 消费者崩溃或退出。Kafka 需要选举新的 Leader,重新进行重平衡
一旦触发Rebalance,就会执行以下流程:
- 1.暂停消费:在重平衡开始之前,Kafka 会暂停所有消费者的拉取操作,以确保不会出现重平衡期间的消息丢失或重复消费。
- 2.计算分区分配方案:Kafka 集群会根据当前消费者组的消费者数量和主题分区数量,计算出每个消费者应该分配的分区列表,以实现分区的负载均衡。
- 3.通知消费者:一旦分区分配方案确定,Kafka 集群会将分配方案发送给每个消费者,告诉它们需要消费的分区列表,并请求它们重新加入消费者组。
- 4.重新分配分区:在消费者重新加入消费者组后,Kafka 集群会将分区分配方案应用到实际的分区分配中,重新分配主题分区给各个消费者
- 5.恢复消费: 最后,Kafka 会恢复所有消费者的拉取操作,允许它们消费分配给自己的分
这个很像我们JVM FullGC之后的STW,所以我们尽可能的要避免重平衡时的STW
kafka的设计是遵循分布式原则CAP里的CP(强一致性),通过 组协调器(Group Coordinator) 管理分区分配,确保每个分区仅被一个消费者消费,依赖心跳机制,响应快(但频繁触发可能导致性能抖动)
2.Kafka重平衡优化
默认情况下,消费者离开后会导致重平衡。但如果开启静态成员,Kafka 不会立即移除该消费者,而是等待一段时间 ( group.instance.id )。这样,如果消费者重启,Kafka 仍然保持它的分区分配,不触发重平衡。
还有就是,Kafka 提供了多种分区分配策略,选择合适的策略可以减少重平衡的影响:
- RangeAssignor (默认): 基于 range 分配,可能导致不均衡
- 按 Topic 分区排序,消费者按字典序排序,平均分配余数优先给前序消费者
例如:Topic "orders" 有5个分区
● 分区排序:P0, P1, P2, P3, P4
消费者名称:["consumer-2", "consumer-1", "consumer-3"]
按字典序排序后:["consumer-1", "consumer-2", "consumer-3"]
按照:先平均分配,余数分配给排序靠前的消费者的逻辑
● 1. 计算基础分配数 = 分区总数 / 消费者数量
● 2. 计算余数 = 分区总数 % 消费者数量
● 3. 前"余数"个消费者多分配1个分区
按照上面的步骤:
● 5/3=1
● 5%3=2
前两个消费多分配1个分区,即:
consumer-1:P0,P1
consumer-2:P2,P3
consumer-3:P4
- RoundRobinAssignor:轮询分配,适用于均匀分布的消费者
- 所有 Topic 的分区按哈希排序,消费者按字典序排序,轮询分配
分区排序 = [P0, P1, P2, P3, P4]
消费者排序 = [C1, C2, C3]
# 轮询顺序:
C1 → P0
C2 → P1
C3 → P2
C1 → P3
C2 → P4
- StickyAssignor: 优先保持之前的分区分配,减少重平衡。
- 优先保留原有分配结果,仅调整变动部分(减少重平衡时分区迁移)
- 假如刚开始是:3 消费者 → 5 分区 ,分配策略同 RoundRobin
分区排序 = [P0, P1, P2, P3, P4]
消费者排序 = [C1, C2, C3]
# 轮询顺序:
C1 → P0
C2 → P1
C3 → P2
C1 → P3
C2 → P4
新增一个消费者后 C4 时:
- 仅从 C1、C2 各迁移 1 个分区给 C4
- 最小化迁移:C1 保留 P0,C2 保留 P1,C3 保留 P2,C4 获得 P3、P4
分区排序 = [P0, P1, P2, P3, P4]
消费者排序 = [C1, C2, C3,C4]
C1 → P0
C2 → P1
C3 → P2
C4 → P3
C4 → P4
- CooperativeStickyAssignor: 渐进式重平衡,不会影响所有消费者,只影响变更的部分
- 该分配策略只在kafka2.4+生效
- 初始分配阶段:
- 消费者组启动时,所有消费者参与分配
- 使用StickyAssignor算法进行初始分配
- 每个消费者获得分区分配
- 开始消费
在具体实践中可以通过:
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "order-group");
props.put("partition.assignment.strategy",
"org.apache.kafka.clients.consumer.RoundRobinAssignor"); // 指定策略
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
同时在kafka最新发布的4.0版本中(2025-03-19),提出了下一代消费者重平衡协议,一方面将分区分配逻辑从客户端移到了服务端,简化客户端更加简单,而且这样服务端也能从全局视角更好的处理重平衡。
另外有个比较大的改进,就是允许消费者独立于其他成员进行重平衡了,这就意味着当一个消费者发生变化时,不再需要暂停整个组,其他消费者可以继续正常工作,提高了系统的可靠性和扩展性。
3.RocketMq的重平衡问题
广播模式下,每个消费者都会消费所有消息,不存在重平衡问题。 但是如果实在默认的集群模式下,消费者在一个消费组中,多个消费者会均摊消费,这时候就涉及重平衡的问题。
RocketMQ 的重平衡机制触发条件:
- 消费者加入/退出组。
- Topic 的队列数变化。
- 定时触发:默认每 20 秒检查一次(通过 RebalanceService 线程)。
- 核心特点:
- 无全局停顿:消费者通过 本地计算 + 异步拉取 实现渐进式重平衡。
- 弱一致性:消费者定期从 NameServer 获取元数据,本地计算分配策略(如均分、环形)。
- 延迟容忍:若消费者宕机,最多需 20 秒触发重平衡(相比 Kafka 的实时响应,延迟更高)。
- 核心特点:
通过其特点也能看出和Kafka不同的是,RocketMQ他只有个定时重平衡的机制,他有一个重平衡检查线程,会自动的每 20s 进行一次重平衡检查,如果发现有消费者新增或离开时,会触发重新分配队列。
重平衡检查机制核心代码:
@Override
public void run() {
log.info(this.getServiceName() + " service started");
long realWaitInterval = waitInterval;
while (!this.isStopped()) {
this.waitForRunning(realWaitInterval);
long interval = System.currentTimeMillis() - lastRebalanceTimestamp;
if (interval < minInterval) {
realWaitInterval = minInterval - interval;
} else {
boolean balanced = this.mqClientFactory.doRebalance();
realWaitInterval = balanced ? waitInterval : minInterval;
lastRebalanceTimestamp = System.currentTimeMillis();
}
}
log.info(this.getServiceName() + " service end");
}
正是因为RocketMQ 定时进行重平衡的,而不是像 Kafka 依赖心跳机制做实时重平衡a,那么就会出现如果一个消费者宕机,最多需要 20s 才能触发重平衡,导致这段时间内消息堆积在已宕机的消费者上,影响吞吐。不过定时也有个好处就是避免很多网络抖动,或者频繁增加、退出消费者等导致的频繁的重平衡。
但是相比于Kafka,RocketMQ的重平衡机制最大的好处是STW的影响很小
由于 RocketMQ 的消费者是通过 异步拉取然后再放到本地队列处理消息的,即使重平衡发生,每个消费者仍然可以继续消费它当前的队列中的消息,只要重平衡的时间足够短,就可以完全消除STW的发生,因为这段时间本地队列中消息还是在正常处理的。一旦重平衡好了,拉取的时候拉取新的队列的消息就行了。
还有就是RocketMQ 在消费者重平衡时是通过默认就是通过局部调整来完成的。当消费者变化时,只有受影响的消费者会重新分配消息队列,其他消费者不受影响。(类似kafka的渐进式重平衡,但是RocketMQ默认就是这样的)
4.Kafka VS RocketMq
Kafka 配置优化示例:
props.put("partition.assignment.strategy", "org.apache.kafka.clients.consumer.RoundRobinAssignor");
props.put("session.timeout.ms", "60000"); // 增大超时时间,减少误触发
props.put("enable.auto.commit", "false"); // 手动提交 Offset,避免重平衡时重复消费
RocketMQ 配置优化示例:
consumer.setAllocateMessageQueueStrategy(new AllocateMessageQueueAveragely());
consumer.setPullBatchSize(32); // 减少拉取频率,降低重平衡压力
5.面试话术
重平衡带来的最大问题就是STW问题,一旦触发重平衡,就会暂停消费,影响吞吐。像Kafka通过协调者监听消费者心跳,能够快速感知消费者的数量变化,进而进行重平衡机制,kafka默认的分区分配策略是基于范围分配,且默认情况下消费者离开后就会触发重平衡,但是如果开启静态成员,那么kafka不会立刻移除该消费者,而是等一段时间,那么其他消费者在就可以正常消费,所以推荐使用StickyAssignor或者渐进式重平衡,不会影响所有消费者而是只影响变更的部分。而RocketMq首先在广播模式下不会有重平衡问题,因为所有的消费者均会消费,但是默认的集群模式下,当消费者组数量发生变化以及Topic的队列数发生变化也会有重平衡问题,rocketMq的消息拉取线程拉到的消息不是直接消费,而是放在ProcessQueue里等待消费者线程去消费,并且有一个定时重平衡线程每20s去检查一次,所以会有20s的缓冲,并结合ProcessQueue,可以很大程度上避免重平衡带来的STW,但是会导致RocketMq反应稍慢,还有就是RocketMq的消费者平衡默认就是通过调整局部来完成的,只有受到影响的消费者会重新分配队列,其他消费者并不受影响,而Kafka需要手动开启,两个方案各有优缺点,像kafka适合实时性要求极高的场景,而RocketMQ适合稳定性要求高的场景。