第12篇 Rebalance 深度解析

第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篇《性能调优------从参数到架构,吞吐量最大化》。

相关推荐
Solis程序员2 小时前
基于 Outbox 事务表 + Canal 监听+kafka+多级缓存:高并发社交关注系统全链路架构设计
分布式·kafka·linq
xG8XPvV5d2 小时前
Kafka重平衡机制深度解析
分布式·kafka
杨运交3 小时前
[019][数据模块]MyBatis-Plus 拦截器扩展设计:基于函数式接口与 Spring 自动装配
spring boot
海兰3 小时前
【第56篇】Graph Example —— MCP-Node 模块
java·人工智能·spring boot·spring ai
倒流时光三十年4 小时前
第四章 WXSS 样式系统与布局
spring boot·微信小程序
勿忘,瞬间4 小时前
Spring日志
java·spring boot·spring
Jackyzhe4 小时前
从零学习Kafka:调优
分布式·学习·kafka
未若君雅裁5 小时前
SpringMVC 执行流程详解
java·spring boot·spring·状态模式