Kafka Rebalance 机制详解
一、Rebalance 基本概念
1. 什么是 Rebalance
Rebalance 是 Kafka 消费者组的一种分区重分配机制,当消费者组的状态发生变化时,触发所有分区在所有消费者之间重新分配,以达到负载均衡的目的。
#mermaid-svg-QFzb6Oldj2pv2YTK{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QFzb6Oldj2pv2YTK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QFzb6Oldj2pv2YTK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QFzb6Oldj2pv2YTK .error-icon{fill:#552222;}#mermaid-svg-QFzb6Oldj2pv2YTK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QFzb6Oldj2pv2YTK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QFzb6Oldj2pv2YTK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QFzb6Oldj2pv2YTK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QFzb6Oldj2pv2YTK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QFzb6Oldj2pv2YTK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QFzb6Oldj2pv2YTK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QFzb6Oldj2pv2YTK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QFzb6Oldj2pv2YTK .marker.cross{stroke:#333333;}#mermaid-svg-QFzb6Oldj2pv2YTK svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QFzb6Oldj2pv2YTK p{margin:0;}#mermaid-svg-QFzb6Oldj2pv2YTK .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-QFzb6Oldj2pv2YTK .cluster-label text{fill:#333;}#mermaid-svg-QFzb6Oldj2pv2YTK .cluster-label span{color:#333;}#mermaid-svg-QFzb6Oldj2pv2YTK .cluster-label span p{background-color:transparent;}#mermaid-svg-QFzb6Oldj2pv2YTK .label text,#mermaid-svg-QFzb6Oldj2pv2YTK span{fill:#333;color:#333;}#mermaid-svg-QFzb6Oldj2pv2YTK .node rect,#mermaid-svg-QFzb6Oldj2pv2YTK .node circle,#mermaid-svg-QFzb6Oldj2pv2YTK .node ellipse,#mermaid-svg-QFzb6Oldj2pv2YTK .node polygon,#mermaid-svg-QFzb6Oldj2pv2YTK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QFzb6Oldj2pv2YTK .rough-node .label text,#mermaid-svg-QFzb6Oldj2pv2YTK .node .label text,#mermaid-svg-QFzb6Oldj2pv2YTK .image-shape .label,#mermaid-svg-QFzb6Oldj2pv2YTK .icon-shape .label{text-anchor:middle;}#mermaid-svg-QFzb6Oldj2pv2YTK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QFzb6Oldj2pv2YTK .rough-node .label,#mermaid-svg-QFzb6Oldj2pv2YTK .node .label,#mermaid-svg-QFzb6Oldj2pv2YTK .image-shape .label,#mermaid-svg-QFzb6Oldj2pv2YTK .icon-shape .label{text-align:center;}#mermaid-svg-QFzb6Oldj2pv2YTK .node.clickable{cursor:pointer;}#mermaid-svg-QFzb6Oldj2pv2YTK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QFzb6Oldj2pv2YTK .arrowheadPath{fill:#333333;}#mermaid-svg-QFzb6Oldj2pv2YTK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QFzb6Oldj2pv2YTK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QFzb6Oldj2pv2YTK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QFzb6Oldj2pv2YTK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QFzb6Oldj2pv2YTK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QFzb6Oldj2pv2YTK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QFzb6Oldj2pv2YTK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QFzb6Oldj2pv2YTK .cluster text{fill:#333;}#mermaid-svg-QFzb6Oldj2pv2YTK .cluster span{color:#333;}#mermaid-svg-QFzb6Oldj2pv2YTK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QFzb6Oldj2pv2YTK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QFzb6Oldj2pv2YTK rect.text{fill:none;stroke-width:0;}#mermaid-svg-QFzb6Oldj2pv2YTK .icon-shape,#mermaid-svg-QFzb6Oldj2pv2YTK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QFzb6Oldj2pv2YTK .icon-shape p,#mermaid-svg-QFzb6Oldj2pv2YTK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QFzb6Oldj2pv2YTK .icon-shape rect,#mermaid-svg-QFzb6Oldj2pv2YTK .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QFzb6Oldj2pv2YTK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QFzb6Oldj2pv2YTK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QFzb6Oldj2pv2YTK :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Rebalance 后 – 负载均衡
Consumer 1
Partition 0
Partition 1
Consumer 2
Partition 2
Partition 3
Consumer 3
Partition 4
Partition 5
Rebalance 中 – 暂停消费
暂停
暂停
暂停
暂停
暂停
暂停
重新分配
Consumer 1
Partition 0
Partition 1
Partition 2
Consumer 2
Partition 3
Partition 4
Consumer 3
Partition 5
Group Coordinator
ALL
Rebalance 前 – 负载不均
空闲
Consumer 1
Partition 0
Partition 1
Partition 2
Partition 3
Partition 4
Consumer 2
Partition 5
Consumer 3
无分区分配
2. Rebalance 核心目标
| 负载均衡 | 确保每个消费者处理大致相等数量的分区 |
| 故障转移 | 消费者故障时,其分区被其他消费者接管 |
| 弹性伸缩 | 新增消费者时,自动分担负载 |
| 分区再分配 | Topic 分区数变化时,重新分配 |
二、Rebalance 的三种核心策略
1. Range 策略(范围分配)
原理
Range 策略是基于单个 Topic 的分区分配策略,它将每个 Topic 的分区按照消费者顺序进行范围划分。
分配算法
// 伪代码:Range 分配算法
// 对于每个 Topic:
// 1. 对消费者按字典序排序 [C0, C1, C2]
// 2. 计算每个消费者分配的分区数 = 分区数 / 消费者数
// 3. 余数分配给前几个消费者
// 示例:TopicA 有 5 个分区 [0,1,2,3,4],3 个消费者
// 每个消费者应得 = 5/3 = 1 个分区,余数 2
// 分配结果:
// C0: [0,1] (多分一个)
// C1: [2,3] (多分一个)
// C2: [4] (少分一个)
配置方式
// 消费者配置
props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
RangeAssignor.class.getName());
// Spring Boot 配置
spring:
kafka:
consumer:
properties:
partition.assignment.strategy: org.apache.kafka.clients.consumer.RangeAssignor
优缺点
| 实现简单,易于理解 | 存在分配不均问题(余数分配) |
| 同一个 Topic 的分区尽量集中 | 多个 Topic 时可能造成某个消费者负载过重 |
| 适合分区数少的场景 | 消费者增减时影响范围大 |
2. RoundRobin 策略(轮询分配)
原理
RoundRobin 策略将所有 Topic 的所有分区视为一个列表,轮询分配给所有消费者。
分配算法
// 伪代码:RoundRobin 分配算法
// 1. 收集所有订阅 Topic 的所有分区
// 2. 消费者按字典序排序
// 3. 轮询分配每个分区给下一个消费者
// 示例:
// TopicA: [0,1,2], TopicB: [0,1], 消费者 [C0, C1]
// 所有分区列表: [A0, A1, A2, B0, B1]
// 轮询分配:
// A0 -> C0
// A1 -> C1
// A2 -> C0
// B0 -> C1
// B1 -> C0
// 结果: C0: [A0, A2, B1], C1: [A1, B0]
配置方式
// 消费者配置
props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
RoundRobinAssignor.class.getName());
// Spring Boot 配置
spring:
kafka:
consumer:
properties:
partition.assignment.strategy: org.apache.kafka.clients.consumer.RoundRobinAssignor
优缺点
| 分配最均匀,负载均衡效果好 | 每次 Rebalance 都需要全量计算 |
| 跨 Topic 的负载均衡 | 消费者订阅不同 Topic 时可能无效 |
| 适合多 Topic 场景 | 计算复杂度较高 |
3. Sticky 策略(粘性分配)
原理
Sticky 策略在保证负载均衡的前提下,尽可能保留上一次的分配结果,最小化分区移动。
核心原则
分配算法
// 伪代码:Sticky 分配算法
// 1. 保留现有分配中仍然有效的部分
// 2. 计算需要重新分配的剩余分区
// 3. 按负载均衡原则分配剩余分区
// 示例:
// 初始分配: C0: [A0, A1], C1: [A2, B0]
// 新增 C2 消费者
// Sticky 策略会尽量保留:
// C0: [A0, A1] (保持不动)
// C1: [A2] (只移动 B0)
// C2: [B0] (接收移动的分区)
配置方式
// 消费者配置
props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
StickyAssignor.class.getName());
// Spring Boot 配置
spring:
kafka:
consumer:
properties:
partition.assignment.strategy: org.apache.kafka.clients.consumer.StickyAssignor
// 协同式粘性分配(推荐)
props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
CooperativeStickyAssignor.class.getName());
优缺点
| 最小化分区移动,减少开销 | 算法复杂,实现难度大 |
| 减少重复消费和空消费时间 | 需要消费者版本支持 |
| 负载均衡效果好 | 协调开销略大 |
| 协同式支持渐进式 Rebalance | – |
4. 三种策略对比总结
| 分配粒度 | 按 Topic | 全部分区 | 全部分区 |
| 均匀性 | 一般 | 优秀 | 优秀 |
| 移动成本 | 高 | 高 | 低 |
| 计算复杂度 | 低 | 中 | 高 |
| 适用场景 | 单 Topic | 多 Topic 均匀 | 通用推荐 |
| Rebalance 时间 | 中 | 中 | 短 |
| 消费者增减影响 | 大 | 中 | 小 |
三、触发 Rebalance 的原因
1. 消费者数量变化
#mermaid-svg-XmOLcQHimRSTGH5h{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XmOLcQHimRSTGH5h .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XmOLcQHimRSTGH5h .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XmOLcQHimRSTGH5h .error-icon{fill:#552222;}#mermaid-svg-XmOLcQHimRSTGH5h .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XmOLcQHimRSTGH5h .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XmOLcQHimRSTGH5h .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XmOLcQHimRSTGH5h .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XmOLcQHimRSTGH5h .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XmOLcQHimRSTGH5h .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XmOLcQHimRSTGH5h .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XmOLcQHimRSTGH5h .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XmOLcQHimRSTGH5h .marker.cross{stroke:#333333;}#mermaid-svg-XmOLcQHimRSTGH5h svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XmOLcQHimRSTGH5h p{margin:0;}#mermaid-svg-XmOLcQHimRSTGH5h .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-XmOLcQHimRSTGH5h .cluster-label text{fill:#333;}#mermaid-svg-XmOLcQHimRSTGH5h .cluster-label span{color:#333;}#mermaid-svg-XmOLcQHimRSTGH5h .cluster-label span p{background-color:transparent;}#mermaid-svg-XmOLcQHimRSTGH5h .label text,#mermaid-svg-XmOLcQHimRSTGH5h span{fill:#333;color:#333;}#mermaid-svg-XmOLcQHimRSTGH5h .node rect,#mermaid-svg-XmOLcQHimRSTGH5h .node circle,#mermaid-svg-XmOLcQHimRSTGH5h .node ellipse,#mermaid-svg-XmOLcQHimRSTGH5h .node polygon,#mermaid-svg-XmOLcQHimRSTGH5h .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XmOLcQHimRSTGH5h .rough-node .label text,#mermaid-svg-XmOLcQHimRSTGH5h .node .label text,#mermaid-svg-XmOLcQHimRSTGH5h .image-shape .label,#mermaid-svg-XmOLcQHimRSTGH5h .icon-shape .label{text-anchor:middle;}#mermaid-svg-XmOLcQHimRSTGH5h .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XmOLcQHimRSTGH5h .rough-node .label,#mermaid-svg-XmOLcQHimRSTGH5h .node .label,#mermaid-svg-XmOLcQHimRSTGH5h .image-shape .label,#mermaid-svg-XmOLcQHimRSTGH5h .icon-shape .label{text-align:center;}#mermaid-svg-XmOLcQHimRSTGH5h .node.clickable{cursor:pointer;}#mermaid-svg-XmOLcQHimRSTGH5h .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XmOLcQHimRSTGH5h .arrowheadPath{fill:#333333;}#mermaid-svg-XmOLcQHimRSTGH5h .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XmOLcQHimRSTGH5h .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XmOLcQHimRSTGH5h .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XmOLcQHimRSTGH5h .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XmOLcQHimRSTGH5h .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XmOLcQHimRSTGH5h .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XmOLcQHimRSTGH5h .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XmOLcQHimRSTGH5h .cluster text{fill:#333;}#mermaid-svg-XmOLcQHimRSTGH5h .cluster span{color:#333;}#mermaid-svg-XmOLcQHimRSTGH5h div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XmOLcQHimRSTGH5h .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XmOLcQHimRSTGH5h rect.text{fill:none;stroke-width:0;}#mermaid-svg-XmOLcQHimRSTGH5h .icon-shape,#mermaid-svg-XmOLcQHimRSTGH5h .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XmOLcQHimRSTGH5h .icon-shape p,#mermaid-svg-XmOLcQHimRSTGH5h .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XmOLcQHimRSTGH5h .icon-shape rect,#mermaid-svg-XmOLcQHimRSTGH5h .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XmOLcQHimRSTGH5h .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XmOLcQHimRSTGH5h .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XmOLcQHimRSTGH5h :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
处理流程
Group Coordinator 检测
触发 Rebalance
重新分配分区
消费者恢复消费
触发场景
消费者数量变化
新消费者加入
消费者主动退出
消费者崩溃/超时
消费者取消订阅
(1)新消费者加入
// 场景:新增消费者实例
// 触发条件
props.put(ConsumerConfig.GROUP_ID_CONFIG, "my-group");
props.put(ConsumerConfig.CLIENT_ID_CONFIG, "consumer-3");
// 加入组流程
// 1. 消费者向 Coordinator 发送 JoinGroup 请求
// 2. Coordinator 检测到组变化
// 3. 触发 Rebalance,重新分配分区
(2)消费者离开/故障
| 主动关闭 | 发送 LeaveGroup 请求 | 立即 | 立即触发 Rebalance |
| 会话超时 | 心跳超时 | session.timeout.ms (默认45秒) | 超时后触发 |
| Poll 超时 | poll() 间隔超时 | max.poll.interval.ms (默认5分钟) | 超时后触发 |
| 网络分区 | 网络不可达 | 取决于网络配置 | 超时后触发 |
2. Topic 分区数变化
# 场景:增加 Topic 分区
bin/kafka-topics.sh –alter \\
–topic my-topic \\
–bootstrap-server localhost:9092 \\
–partitions 6
# 触发效果
# 1. 新增分区没有消费者
# 2. Group Coordinator 检测到分区变化
# 3. 触发 Rebalance 分配新增分区
3. 订阅关系变化
// 场景:动态修改订阅
consumer.subscribe(Arrays.asList("topic1", "topic2")); // 初始订阅
// 修改订阅
consumer.subscribe(Arrays.asList("topic1", "topic3")); // 触发 Rebalance
// 取消订阅
consumer.unsubscribe(); // 触发 Rebalance
4. Group Coordinator 变更
#mermaid-svg-UfrLZo5w976wdlgq{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UfrLZo5w976wdlgq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UfrLZo5w976wdlgq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UfrLZo5w976wdlgq .error-icon{fill:#552222;}#mermaid-svg-UfrLZo5w976wdlgq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UfrLZo5w976wdlgq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UfrLZo5w976wdlgq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UfrLZo5w976wdlgq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UfrLZo5w976wdlgq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UfrLZo5w976wdlgq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UfrLZo5w976wdlgq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UfrLZo5w976wdlgq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UfrLZo5w976wdlgq .marker.cross{stroke:#333333;}#mermaid-svg-UfrLZo5w976wdlgq svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UfrLZo5w976wdlgq p{margin:0;}#mermaid-svg-UfrLZo5w976wdlgq .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-UfrLZo5w976wdlgq .cluster-label text{fill:#333;}#mermaid-svg-UfrLZo5w976wdlgq .cluster-label span{color:#333;}#mermaid-svg-UfrLZo5w976wdlgq .cluster-label span p{background-color:transparent;}#mermaid-svg-UfrLZo5w976wdlgq .label text,#mermaid-svg-UfrLZo5w976wdlgq span{fill:#333;color:#333;}#mermaid-svg-UfrLZo5w976wdlgq .node rect,#mermaid-svg-UfrLZo5w976wdlgq .node circle,#mermaid-svg-UfrLZo5w976wdlgq .node ellipse,#mermaid-svg-UfrLZo5w976wdlgq .node polygon,#mermaid-svg-UfrLZo5w976wdlgq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UfrLZo5w976wdlgq .rough-node .label text,#mermaid-svg-UfrLZo5w976wdlgq .node .label text,#mermaid-svg-UfrLZo5w976wdlgq .image-shape .label,#mermaid-svg-UfrLZo5w976wdlgq .icon-shape .label{text-anchor:middle;}#mermaid-svg-UfrLZo5w976wdlgq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UfrLZo5w976wdlgq .rough-node .label,#mermaid-svg-UfrLZo5w976wdlgq .node .label,#mermaid-svg-UfrLZo5w976wdlgq .image-shape .label,#mermaid-svg-UfrLZo5w976wdlgq .icon-shape .label{text-align:center;}#mermaid-svg-UfrLZo5w976wdlgq .node.clickable{cursor:pointer;}#mermaid-svg-UfrLZo5w976wdlgq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UfrLZo5w976wdlgq .arrowheadPath{fill:#333333;}#mermaid-svg-UfrLZo5w976wdlgq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UfrLZo5w976wdlgq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UfrLZo5w976wdlgq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UfrLZo5w976wdlgq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UfrLZo5w976wdlgq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UfrLZo5w976wdlgq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UfrLZo5w976wdlgq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UfrLZo5w976wdlgq .cluster text{fill:#333;}#mermaid-svg-UfrLZo5w976wdlgq .cluster span{color:#333;}#mermaid-svg-UfrLZo5w976wdlgq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UfrLZo5w976wdlgq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UfrLZo5w976wdlgq rect.text{fill:none;stroke-width:0;}#mermaid-svg-UfrLZo5w976wdlgq .icon-shape,#mermaid-svg-UfrLZo5w976wdlgq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UfrLZo5w976wdlgq .icon-shape p,#mermaid-svg-UfrLZo5w976wdlgq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UfrLZo5w976wdlgq .icon-shape rect,#mermaid-svg-UfrLZo5w976wdlgq .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UfrLZo5w976wdlgq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UfrLZo5w976wdlgq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UfrLZo5w976wdlgq :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Group Coordinator 节点故障
新的 Broker 接管
加载消费者组元数据
触发 Rebalance
所有消费者重新连接
四、Rebalance 详细流程
1. 完整 Rebalance 时序图
Metadata StoreGroup CoordinatorConsumer 3Consumer 2Consumer 1Metadata StoreGroup CoordinatorConsumer 3Consumer 2Consumer 1#mermaid-svg-YsEfNSJEgVgJUVp2{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YsEfNSJEgVgJUVp2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YsEfNSJEgVgJUVp2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YsEfNSJEgVgJUVp2 .error-icon{fill:#552222;}#mermaid-svg-YsEfNSJEgVgJUVp2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YsEfNSJEgVgJUVp2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YsEfNSJEgVgJUVp2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YsEfNSJEgVgJUVp2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YsEfNSJEgVgJUVp2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YsEfNSJEgVgJUVp2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YsEfNSJEgVgJUVp2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YsEfNSJEgVgJUVp2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YsEfNSJEgVgJUVp2 .marker.cross{stroke:#333333;}#mermaid-svg-YsEfNSJEgVgJUVp2 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YsEfNSJEgVgJUVp2 p{margin:0;}#mermaid-svg-YsEfNSJEgVgJUVp2 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-YsEfNSJEgVgJUVp2 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-YsEfNSJEgVgJUVp2 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-YsEfNSJEgVgJUVp2 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-YsEfNSJEgVgJUVp2 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-YsEfNSJEgVgJUVp2 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-YsEfNSJEgVgJUVp2 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-YsEfNSJEgVgJUVp2 .sequenceNumber{fill:white;}#mermaid-svg-YsEfNSJEgVgJUVp2 #sequencenumber{fill:#333;}#mermaid-svg-YsEfNSJEgVgJUVp2 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-YsEfNSJEgVgJUVp2 .messageText{fill:#333;stroke:none;}#mermaid-svg-YsEfNSJEgVgJUVp2 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-YsEfNSJEgVgJUVp2 .labelText,#mermaid-svg-YsEfNSJEgVgJUVp2 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-YsEfNSJEgVgJUVp2 .loopText,#mermaid-svg-YsEfNSJEgVgJUVp2 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-YsEfNSJEgVgJUVp2 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-YsEfNSJEgVgJUVp2 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-YsEfNSJEgVgJUVp2 .noteText,#mermaid-svg-YsEfNSJEgVgJUVp2 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-YsEfNSJEgVgJUVp2 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-YsEfNSJEgVgJUVp2 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-YsEfNSJEgVgJUVp2 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-YsEfNSJEgVgJUVp2 .actorPopupMenu{position:absolute;}#mermaid-svg-YsEfNSJEgVgJUVp2 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-YsEfNSJEgVgJUVp2 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-YsEfNSJEgVgJUVp2 .actor-man circle,#mermaid-svg-YsEfNSJEgVgJUVp2 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-YsEfNSJEgVgJUVp2 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}正常消费阶段触发 Rebalancepar[JoinGroup 阶段]Leader 执行分区分配par[新分配生效]恢复正常消费心跳超时/离开检测到消费者变更心跳响应: REBALANCE_IN_PROGRESS心跳响应: REBALANCE_IN_PROGRESSJoinGroup (选举 Leader)JoinGroup (作为 Follower)成为 Leader (包含成员列表)SyncGroup (上传分配方案)SyncGroup (下发分配方案)连接超时,踢出组提交新 offset提交新 offset
2. Rebalance 阶段详解
阶段一:发现阶段 (Detection)
// Coordinator 检测到消费者变化
// 1. 心跳超时检测
// 2. 主动离开请求
// 3. 订阅变更请求
阶段二:JoinGroup 阶段
// 消费者发送 JoinGroup 请求
JoinGroupRequest request = new JoinGroupRequest()
.setGroupId("my-group")
.setMemberId(currentMemberId)
.setProtocolType("consumer")
.setProtocols( subscriptions );
// Coordinator 响应
// – 指定 Leader 消费者
// – 返回当前组成员列表
阶段三:SyncGroup 阶段
// Leader 消费者执行分区分配
PartitionAssignor assignor = new RangeAssignor();
Map<String, Assignment> assignments =
assignor.assign(metadata, groupSubscription);
// Leader 发送分配结果给 Coordinator
SyncGroupRequest request = new SyncGroupRequest()
.setGroupId("my-group")
.setMemberId(leaderId)
.setAssignments(assignments);
// Coordinator 广播给所有消费者
阶段四:稳定阶段 (Stable)
// 消费者收到分配结果
// 1. 撤销原有分区
// 2. 分配新分区
// 3. 开始消费
// 4. 恢复正常心跳
五、Rebalance 的优缺点
1. 优点
| 自动负载均衡 | 消费者负载自动调整,无需人工干预 | 新增消费者自动分担压力 |
| 高可用性 | 消费者故障时自动转移分区 | 某消费者宕机,其他接管 |
| 弹性伸缩 | 支持动态扩缩容 | 业务高峰期增加消费者 |
| 分区变化适应 | Topic 扩容自动分配 | 从3分区扩到6分区 |
| 容错性 | 网络闪断后自动恢复 | 消费者重启后重新加入 |
2. 缺点
| Stop-The-World | Rebalance 期间所有消费者暂停消费 | 高 |
| 重复消费 | 分区重新分配导致消息被多次处理 | 中 |
| 消费延迟 | Rebalance 期间消息积压 | 高 |
| 频繁 Rebalance | 配置不当导致频繁触发 | 中 |
| 数据倾斜 | 分配不均导致部分消费者过载 | 低 |
| 状态丢失 | 本地状态需要重建 | 中 |
3. Rebalance 代价分析
// Rebalance 代价计算
class RebalanceCost {
// 计算 Rebalance 总代价
long calculateTotalCost(RebalanceEvent event) {
// 1. 暂停消费时间
long stopTime = event.getDuration();
// 2. 重复消费代价
long duplicateMessages = event.getReassignedPartitions()
.stream()
.mapToLong(p -> p.getLastProcessedOffset() – p.getLastCommittedOffset())
.sum();
// 3. 状态重建代价
long stateRebuildTime = event.getStatefulConsumers()
.stream()
.mapToLong(c -> c.rebuildState())
.sum();
// 4. 网络开销
long networkCost = event.getMembers() *
(JOIN_REQUEST_SIZE + SYNC_REQUEST_SIZE);
return stopTime + duplicateMessages * 10 + stateRebuildTime + networkCost;
}
}
六、Rebalance 优化策略
1. 参数优化
# 消费者配置优化
# 心跳相关(减少误判)
session.timeout.ms=45000 # 会话超时(适当增大)
heartbeat.interval.ms=3000 # 心跳间隔(session的1/3)
max.poll.interval.ms=300000 # 最大 poll 间隔(5分钟)
# Rebalance 相关
partition.assignment.strategy=org.apache.kafka.clients.consumer.StickyAssignor # 粘性分配
max.poll.records=500 # 每次 poll 最大记录数(防止处理过慢)
# 超时设置
default.api.timeout.ms=60000 # API 超时
request.timeout.ms=30000 # 请求超时
# 连接优化
reconnect.backoff.ms=50 # 重连退避
reconnect.backoff.max.ms=1000 # 最大重连退避
2. 静态成员配置
// 静态成员(Static Membership)- 减少 Rebalance
props.put(ConsumerConfig.GROUP_INSTANCE_ID_CONFIG, "consumer-1-static");
// 优点:消费者重启时保留分区分配
// 适用场景:重要消费者,频繁重启的场景
3. 渐进式 Rebalance
// 协同式粘性分配(Cooperative Sticky)
props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
CooperativeStickyAssignor.class.getName());
// 特点:
// – 分批撤销分区
// – 减少 STW 时间
// – 部分消费者可继续消费
4. 业务层优化
@Component
public class OptimizedConsumer {
@KafkaListener(topics = "my-topic")
public void consume(ConsumerRecord<String, String> record) {
try {
// 1. 幂等处理(防止重复消费)
if (isProcessed(record)) {
return;
}
// 2. 快速处理,避免 poll 超时
processWithTimeout(record, 1000);
// 3. 异步提交 offset
commitOffsetAsync(record);
} catch (TimeoutException e) {
// 4. 超时处理,避免触发 Rebalance
log.warn("处理超时,稍后重试");
throw new RetryableException(e);
}
}
// 5. 监听 Rebalance 事件
@KafkaListener(topics = "my-topic")
public void consumeWithRebalanceListener(ConsumerRecord<String, String> record,
Consumer consumer) {
consumer.subscribe(Arrays.asList("my-topic"), new ConsumerRebalanceListener() {
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
// 分区被撤销前:提交 offset,清理状态
consumer.commitSync();
clearLocalState(partitions);
}
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
// 新分区分配后:初始化状态
initializeLocalState(partitions);
}
});
}
}
七、Rebalance 监控与排查
1. 监控指标
# 1. 查看消费者组状态
bin/kafka-consumer-groups.sh –describe –group my-group –bootstrap-server localhost:9092
# 输出关键指标
# – LAG: 积压消息数(Rebalance 期间会增大)
# – CURRENT-OFFSET: 当前偏移量
# – LOG-END-OFFSET: 最新偏移量
# 2. JMX 监控指标
# MBean: kafka.consumer:type=consumer-coordinator-metrics
# – rebalance-total: Rebalance 总次数
# – rebalance-rate-per-hour: 每小时 Rebalance 次数
# – rebalance-latency-avg: 平均 Rebalance 延迟
2. 日志排查
# 查看 Rebalance 相关日志
grep "Rebalance" /var/log/kafka/consumer.log
# 常见日志模式
# 1. 触发 Rebalance
INFO: [Consumer clientId=consumer-1, groupId=my-group]
Preparing to rebalance group
# 2. JoinGroup
INFO: [Consumer clientId=consumer-1, groupId=my-group]
Successfully joined group with generation 5
# 3. 分配结果
INFO: [Consumer clientId=consumer-1, groupId=my-group]
Assigned partitions: [topic-0, topic-1, topic-2]
# 4. 完成 Rebalance
INFO: [Consumer clientId=consumer-1, groupId=my-group]
Completed rebalance in 3456 ms
3. 问题排查清单
| 频繁 Rebalance | session.timeout.ms 太小 | 查看心跳日志 | 增大超时时间 |
| Rebalance 时间过长 | 分区数太多 | 查看分配时间 | 使用 Sticky 策略 |
| 重复消费严重 | 提交 offset 不及时 | 查看 offset 提交日志 | 改为同步提交 |
| 消费者无法加入 | max.poll.interval.ms 太小 | 查看处理时间 | 增大间隔或优化代码 |
| 分配不均 | Range 策略导致 | 查看分配结果 | 改用 RoundRobin/Sticky |
八、最佳实践总结
1. 配置推荐
# 生产环境推荐配置
# 通用配置
session.timeout.ms=45000
heartbeat.interval.ms=15000
max.poll.interval.ms=300000
max.poll.records=500
# 分配策略(推荐)
partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor
# 关键消费者(可选)
group.instance.id=consumer-1-static # 静态成员
# 提交配置
enable.auto.commit=false # 手动提交
auto.commit.interval.ms=5000 # 如果自动提交
2. 代码最佳实践
@Component
public class BestPracticeConsumer {
@KafkaListener(topics = "my-topic")
public void consume(ConsumerRecord<String, String> record,
Acknowledgment ack) {
// 1. 幂等处理
String messageId = record.key();
if (redisUtils.exists(messageId)) {
ack.acknowledge(); // 已处理过,直接提交
return;
}
try {
// 2. 业务处理(设置超时)
CompletableFuture.runAsync(() -> process(record))
.orTimeout(30, TimeUnit.SECONDS)
.join();
// 3. 记录处理状态
redisUtils.set(messageId, "processed", 1, TimeUnit.HOURS);
// 4. 手动提交
ack.acknowledge();
} catch (Exception e) {
// 5. 异常处理
log.error("处理失败", e);
// 根据异常类型决定是否重试
if (isRetryable(e)) {
throw new RetryableException(e); // 触发重试
} else {
sendToDlq(record); // 发送死信队列
ack.acknowledge(); // 避免阻塞
}
}
}
// 6. Rebalance 监听器
@Bean
public ConsumerRebalanceListener rebalanceListener() {
return new ConsumerRebalanceListener() {
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
log.info("分区被撤销: {}", partitions);
// 提交最后的 offset
// 清理本地状态
}
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
log.info("新分配分区: {}", partitions);
// 初始化状态
// 可指定从哪个 offset 开始
}
};
}
}
3. 监控告警配置
# Prometheus 告警规则
groups:
– name: kafka_rebalance_alerts
rules:
# 频繁 Rebalance 告警
– alert: KafkaHighRebalanceRate
expr: rate(kafka_consumer_coordinator_rebalance_total[5m]) > 0.1
for: 10m
annotations:
summary: "高频 Rebalance 检测"
description: "消费者组 {{ $labels.group }} 每5分钟 Rebalance 次数 > 0.1"
# Rebalance 耗时过长
– alert: KafkaSlowRebalance
expr: kafka_consumer_coordinator_rebalance_latency_avg > 10000
for: 5m
annotations:
summary: "Rebalance 耗时过长"
description: "平均 Rebalance 耗时 {{ $value }}ms"
# 消费者 Lag 突增
– alert: KafkaLagSpike
expr: delta(kafka_consumer_lag[5m]) > 10000
for: 2m
annotations:
summary: "消息积压突增"
description: "可能正在 Rebalance,积压增加 {{ $value }}"
4. 性能优化 checklist
- 选择 CooperativeStickyAssignor 策略
- 配置合理的 session.timeout.ms (30-45秒)
- 确保 max.poll.interval.ms > 最大处理时间
- 关键消费者使用 静态成员
- 实现 幂等处理 防止重复消费
- 添加 Rebalance 监听器 处理状态
- 监控 Rebalance 频率和耗时
- 分区数合理(≤ 消费者数 × 消费能力)
- 消费者实例数稳定,避免频繁扩缩容
九、总结
Rebalance 核心要点
| 三种策略 | Range(范围)、RoundRobin(轮询)、Sticky(粘性) |
| 触发原因 | 消费者变化、分区变化、订阅变化、Coordinator变更 |
| 主要缺点 | Stop-The-World、重复消费、延迟增加 |
| 优化方向 | Sticky策略、参数调优、静态成员、幂等处理 |
| 监控重点 | Rebalance频率、耗时、Lag变化 |
Rebalance 是 Kafka 实现自动负载均衡的核心机制,但也是一把双刃剑——它保证了高可用和弹性,但也带来了短暂的服务暂停和重复消费。通过选择合适的分配策略、合理配置参数、实现幂等处理和状态管理,可以将 Rebalance 的影响降到最低。



