第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个Partition3个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 ProtocolKafka 默认直到 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 停止消费 ★ 阶段3Join Group 所有 Consumer 向 Coordinator 重新申请加入 等待所有 Consumer 响应最慢的那个决定整体速度 阶段4分配 Partition Group Leader一般是第一个加入的 Consumer计算新分配方案 将方案提交给 Coordinator 阶段5Sync 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个PartitionConsumer 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 先放弃全部 PartitionStop-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 后恢复配置方式props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,CooperativeStickyAssignor.class.getName());或 application.ymlspring:kafka:consumer:properties:partition.assignment.strategy:org.apache.kafka.clients.consumer.CooperativeStickyAssignor五、减少 Rebalance 的实战策略策略一合理配置超时参数避免被误踢// 核心公式单条最大处理时间 × max.poll.records max.poll.interval.msprops.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策略二使用 CooperativeStickyAssignorprops.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,CooperativeStickyAssignor.class.getName());策略三优雅停机主动离组Consumer 主动调用LeaveGroupRequestCoordinator 立即感知成员离开不需要等session.timeout.ms超时才触发 Rebalance。# Spring Boot 优雅停机配置server:shutdown:graceful# 接收 SIGTERM 后优雅关机spring:lifecycle:timeout-per-shutdown-phase:60s# 等待消费者处理完当前批次// Spring Kafka 会在 Spring 容器关闭时自动停止 Listener// 确保消费完当前批次后再关闭减少重复消费PreDestroypublicvoidshutdown(){log.info(服务开始关闭等待消费者处理完当前批次...);// Spring 自动处理无需手动干预}策略四控制 Consumer 数量不超过 Partition 数Consumer 数 Partition 数 多余的 Consumer 永远空闲拿不到 Partition 但它们仍然参与心跳、参与 Rebalance 任何一个空闲 Consumer 抖动 → 触发 Rebalance → 影响所有 Consumer 规则Consumer 实例数 ≤ Partition 数六、Rebalance 监听器在 Rebalance 前后执行清理/初始化操作BeanpublicConcurrentKafkaListenerContainerFactoryString,Stringfactory(ConsumerFactoryString,StringconsumerFactory){varfactorynewConcurrentKafkaListenerContainerFactoryString,String();factory.setConsumerFactory(consumerFactory);factory.getContainerProperties().setConsumerRebalanceListener(newConsumerAwareRebalanceListener(){OverridepublicvoidonPartitionsRevokedBeforeCommit(Consumer?,?consumer,CollectionTopicPartitionpartitions){// Rebalance 前、Offset 提交前被调用// ★ 最重要的回调在这里提交未提交的 Offset ★log.warn(即将失去分区Rebalance开始: {},partitions);// 注意这里不能直接调用 ack.acknowledge()// Spring Kafka 会在此之后自动处理未提交的 Offset}OverridepublicvoidonPartitionsAssigned(Consumer?,?consumer,CollectionTopicPartitionpartitions){// Rebalance 后获得了这些新分区log.info(获得新分区Rebalance完成: {},partitions);// 可以在这里初始化分区相关的缓存或资源}OverridepublicvoidonPartitionsLost(Consumer?,?consumer,CollectionTopicPartitionpartitions){// 分区被强制撤走Consumer 被踢出时log.error(分区被强制撤走: {},partitions);// 清理这些分区相关的状态}});returnfactory;}七、踩坑记录❌ 坑1Consumer 数量比 Partition 多Rebalance 反而更频繁Topic: 4个Partition Consumer Group: 8个Consumer实例 问题 4个Consumer正在消费另外4个空闲 空闲Consumer持续发心跳任何一个抖动 → Rebalance → 影响所有人 系统更不稳定而不是更高效 解决Consumer 数量 ≤ Partition 数量 要提升吞吐先增加 Partition 数❌ 坑2JVM Full GC 导致心跳停顿触发 RebalanceJVM Full GC 耗时 30 秒STW session.timeout.ms 10 秒 → GC 期间无法发送心跳 → session 超时 → 触发 Rebalance ❌ 解决 方案1换 G1GC 或 ZGC减少 GC 停顿时间 方案2适当调大 session.timeout.ms代价是故障恢复变慢 方案3减少堆内存使用从根本上减少 Full GC❌ 坑3Rebalance 后重复消费Consumer A 处理了 msg1、msg2准备提交 Offset Rebalance 发生Consumer A 失去该 Partition Consumer B 接管从上次提交的 Offset 重新消费 → msg1、msg2 重复 解决 方案1在 onPartitionsRevokedBeforeCommit 中提交 Offset 方案2Consumer 实现幂等设计第10篇 方案3使用 CooperativeStickyAssignor 减少分区迁移 本篇小结方面关键点Rebalance 本质分区重新分配传统协议期间全员 Stop-The-World主要触发原因Consumer 超时被误踢参数配置不当分配策略推荐CooperativeStickyAssignor增量 Rebalance未迁移分区不中断减少 Rebalance合理超时参数 优雅停机 Consumer 数 ≤ Partition 数监听 RebalanceConsumerRebalanceListener在分区变化时做清理和初始化一句话总结Rebalance 无法完全消除但通过 CooperativeStickyAssignor 合理参数配置可以大幅降低它的影响——从 Stop-The-World 变成局部微中断。下篇预告第13篇《性能调优——从参数到架构吞吐量最大化》。