第12篇:Rebalance 深度解析 ------ Stop-The-World 的本质与如何减少它
系列 :Kafka × Spring Boot:参数精讲与生产落地实战
本篇关键词 :Rebalance · Stop-The-World ·CooperativeStickyAssignor· 分区分配策略 · 优雅停机
📌 本篇导读
Kafka 生产环境中,Rebalance 是出现频率最高、影响最严重的问题之一。
你可能遇到过:
- 消费日志里周期性出现
Rebalancing... - Consumer Lag 突然飙升,过一会又恢复
- 同一条消息被处理了两次
- 部分消息延迟几十秒才被消费
这些症状,大概率都跟 Rebalance 有关。本篇彻底讲清楚:Rebalance 是什么、为什么发生、影响多大、如何减少。
一、Rebalance 是什么?
Rebalance 是 Kafka Consumer Group 在成员变化时,重新分配 Partition 的过程。
初始状态(Topic 有6个Partition,3个Consumer):
Consumer A → Partition 0, Partition 1
Consumer B → Partition 2, Partition 3
Consumer C → Partition 4, Partition 5
新增 Consumer D → 触发 Rebalance:
Consumer A → Partition 0
Consumer B → Partition 2
Consumer C → Partition 4
Consumer D → Partition 1(接手原来A的一个)
Consumer A 宕机 → 触发 Rebalance:
Consumer B → Partition 0, Partition 2
Consumer C → Partition 1, Partition 4
Consumer D → Partition 3, Partition 5
听起来合理------成员变了,重新分配,有什么问题?
问题在于:Rebalance 期间整个 Consumer Group 停止消费(Stop-The-World)。
二、Rebalance 的 Stop-The-World 过程
传统 Eager Protocol(Kafka 默认,直到 2.4 版本之前)
阶段1:触发 Rebalance
Coordinator 在下次心跳响应中通知所有 Consumer:「需要 Rebalance 了」
阶段2:所有 Consumer 放弃全部 Partition
Consumer A 放弃 Partition 0, 1
Consumer B 放弃 Partition 2, 3
Consumer C 放弃 Partition 4, 5
★ 此时整个 Group 停止消费 ★
阶段3:Join Group
所有 Consumer 向 Coordinator 重新申请加入
等待所有 Consumer 响应(最慢的那个决定整体速度)
阶段4:分配 Partition
Group Leader(一般是第一个加入的 Consumer)计算新分配方案
将方案提交给 Coordinator
阶段5:Sync Group
所有 Consumer 向 Coordinator 索取自己的分配结果
阶段6:开始消费新分配的 Partition
★ Stop-The-World 结束 ★
整个过程:几秒到几分钟,期间消息积压,Lag 飙升
三、Rebalance 的三个触发原因
原因一:Consumer Group 成员变化(最常见)
✅ 新增 Consumer 实例(服务扩容)
→ 必然触发,这是正常的
→ 可以优化 Rebalance 的影响,但无法避免
✅ Consumer 实例下线(服务缩容、停机)
→ 必然触发,这是正常的
→ 优雅停机可以减少 Rebalance 等待时间
❌ Consumer 被「误踢」(最需要优化的场景)
→ session.timeout.ms 超时:网络抖动/GC 停顿导致心跳断连
→ max.poll.interval.ms 超时:业务处理时间过长
→ 这类 Rebalance 是不必要的,应该通过参数优化消除
原因二:Consumer 订阅变化
运行时动态修改了订阅的 Topic → Rebalance
(不推荐在运行时修改订阅)
原因三:Partition 数量变化
对 Topic 进行扩容(增加 Partition 数)→ Rebalance
(只能增加 Partition,无法减少)
四、三种分区分配策略
通过 partition.assignment.strategy 配置,影响 Rebalance 时的分配行为。
策略一:RangeAssignor(默认)
Topic A: 3个Partition,Consumer C1、C2
分配:
C1 → P0, P1(连续范围,数量多)
C2 → P2 (数量少)
问题:
多个 Topic 时,C1 始终比 C2 多分配一个 Partition
→ 负载不均衡
Rebalance 行为:
每次都完全重算,原来的分配结果不保留
策略二:RoundRobinAssignor
将所有 Topic 的所有 Partition 混合后,轮询分配给 Consumer
优点:负载更均衡
缺点:Rebalance 后分配变化大(和原来完全不同),分区迁移代价大
策略三:StickyAssignor(推荐)
原则:在保证负载均衡的前提下,尽量保持上次的分配结果不变
示例:
初始分配:C1 → P0,P1,P2 C2 → P3,P4,P5
C2 下线后 Rebalance:
普通策略(RangeAssignor/RoundRobin):
C1 → P0,P1,P2,P3,P4,P5(全部重新计算)
StickyAssignor:
C1 → P0,P1,P2(保持不变)+ P3,P4,P5(从C2接管)
✓ C1 原来持有的分区不动,只增加新接管的分区
✓ 减少分区迁移,降低 Rebalance 代价
策略四:CooperativeStickyAssignor(强烈推荐,Kafka 2.4+)
与 StickyAssignor 类似的分配算法,但使用 Cooperative Protocol(增量 Rebalance),是最大的改进:
传统 Eager Protocol(旧):
所有 Consumer 先放弃全部 Partition(Stop-The-World)
重新申请加入,分配完成后再开始消费
→ 期间全部停止,影响大
Cooperative Protocol(新):
只有需要「被迁移」的 Partition 停止消费
不需要迁移的 Partition 继续消费,不中断!
示例:
C1→P0,P1,P2 C2→P3,P4,P5
C3 新加入,Rebalance 需要把 P2、P5 迁移给 C3
Eager Protocol:
所有 6 个 Partition 全部停止 → 重新分配 → 恢复
Cooperative Protocol:
只有 P2、P5 停止
P0、P1、P3、P4 继续消费 ✓(完全不中断!)
P2、P5 迁移给 C3 后恢复
配置方式:
java
props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
CooperativeStickyAssignor.class.getName());
或 application.yml:
yaml
spring:
kafka:
consumer:
properties:
partition.assignment.strategy: >
org.apache.kafka.clients.consumer.CooperativeStickyAssignor
五、减少 Rebalance 的实战策略
策略一:合理配置超时参数,避免被误踢
java
// 核心公式:单条最大处理时间 × max.poll.records < max.poll.interval.ms
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 10); // 减少每批消息数
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 600000); // 10分钟
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 45000); // 45秒
props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 15000); // 15秒(45/3)
策略二:使用 CooperativeStickyAssignor
java
props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
CooperativeStickyAssignor.class.getName());
策略三:优雅停机,主动离组
Consumer 主动调用 LeaveGroupRequest,Coordinator 立即感知成员离开,不需要等 session.timeout.ms 超时才触发 Rebalance。
yaml
# Spring Boot 优雅停机配置
server:
shutdown: graceful # 接收 SIGTERM 后优雅关机
spring:
lifecycle:
timeout-per-shutdown-phase: 60s # 等待消费者处理完当前批次
java
// Spring Kafka 会在 Spring 容器关闭时自动停止 Listener
// 确保消费完当前批次后再关闭,减少重复消费
@PreDestroy
public void shutdown() {
log.info("服务开始关闭,等待消费者处理完当前批次...");
// Spring 自动处理,无需手动干预
}
策略四:控制 Consumer 数量不超过 Partition 数
Consumer 数 > Partition 数:
多余的 Consumer 永远空闲(拿不到 Partition)
但它们仍然参与心跳、参与 Rebalance
任何一个空闲 Consumer 抖动 → 触发 Rebalance → 影响所有 Consumer
规则:Consumer 实例数 ≤ Partition 数
六、Rebalance 监听器
在 Rebalance 前后执行清理/初始化操作:
java
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> factory(
ConsumerFactory<String, String> consumerFactory) {
var factory = new ConcurrentKafkaListenerContainerFactory<String, String>();
factory.setConsumerFactory(consumerFactory);
factory.getContainerProperties().setConsumerRebalanceListener(
new ConsumerAwareRebalanceListener() {
@Override
public void onPartitionsRevokedBeforeCommit(
Consumer<?, ?> consumer,
Collection<TopicPartition> partitions) {
// Rebalance 前、Offset 提交前被调用
// ★ 最重要的回调:在这里提交未提交的 Offset ★
log.warn("即将失去分区(Rebalance开始): {}", partitions);
// 注意:这里不能直接调用 ack.acknowledge()
// Spring Kafka 会在此之后自动处理未提交的 Offset
}
@Override
public void onPartitionsAssigned(
Consumer<?, ?> consumer,
Collection<TopicPartition> partitions) {
// Rebalance 后,获得了这些新分区
log.info("获得新分区(Rebalance完成): {}", partitions);
// 可以在这里初始化分区相关的缓存或资源
}
@Override
public void onPartitionsLost(
Consumer<?, ?> consumer,
Collection<TopicPartition> partitions) {
// 分区被强制撤走(Consumer 被踢出时)
log.error("分区被强制撤走: {}", partitions);
// 清理这些分区相关的状态
}
}
);
return factory;
}
七、踩坑记录
❌ 坑1:Consumer 数量比 Partition 多,Rebalance 反而更频繁
Topic: 4个Partition
Consumer Group: 8个Consumer实例
问题:
4个Consumer正在消费,另外4个空闲
空闲Consumer持续发心跳,任何一个抖动 → Rebalance → 影响所有人
系统更不稳定,而不是更高效
解决:Consumer 数量 ≤ Partition 数量
要提升吞吐,先增加 Partition 数
❌ 坑2:JVM Full GC 导致心跳停顿触发 Rebalance
JVM Full GC 耗时 30 秒(STW)
session.timeout.ms = 10 秒
→ GC 期间无法发送心跳 → session 超时 → 触发 Rebalance ❌
解决:
方案1:换 G1GC 或 ZGC,减少 GC 停顿时间
方案2:适当调大 session.timeout.ms(代价是故障恢复变慢)
方案3:减少堆内存使用,从根本上减少 Full GC
❌ 坑3:Rebalance 后重复消费
Consumer A 处理了 msg1、msg2,准备提交 Offset
Rebalance 发生,Consumer A 失去该 Partition
Consumer B 接管,从上次提交的 Offset 重新消费 → msg1、msg2 重复!
解决:
方案1:在 onPartitionsRevokedBeforeCommit 中提交 Offset
方案2:Consumer 实现幂等设计(第10篇)
方案3:使用 CooperativeStickyAssignor 减少分区迁移
📝 本篇小结
| 方面 | 关键点 |
|---|---|
| Rebalance 本质 | 分区重新分配,传统协议期间全员 Stop-The-World |
| 主要触发原因 | Consumer 超时被误踢(参数配置不当) |
| 分配策略推荐 | CooperativeStickyAssignor:增量 Rebalance,未迁移分区不中断 |
| 减少 Rebalance | 合理超时参数 + 优雅停机 + Consumer 数 ≤ Partition 数 |
| 监听 Rebalance | ConsumerRebalanceListener 在分区变化时做清理和初始化 |
一句话总结:Rebalance 无法完全消除,但通过 CooperativeStickyAssignor + 合理参数配置,可以大幅降低它的影响------从 Stop-The-World 变成局部微中断。
下篇预告:第13篇《性能调优------从参数到架构,吞吐量最大化》。