Kafka Rebalance 机制详解
一、Rebalance 基本概念
1. 什么是 Rebalance
Rebalance 是 Kafka 消费者组的一种分区重分配机制,当消费者组的状态发生变化时,触发所有分区在所有消费者之间重新分配,以达到负载均衡的目的。
graph TB
subgraph "Rebalance 前 - 负载不均"
direction TB
C1[Consumer 1] --> P0[Partition 0]
C1 --> P1[Partition 1]
C1 --> P2[Partition 2]
C1 --> P3[Partition 3]
C1 --> P4[Partition 4]
C2[Consumer 2] --> P5[Partition 5]
C3[Consumer 3]
C3 -.->|空闲| X[无分区分配]
style C1 fill:#f96,stroke:#333
style C2 fill:#9cf,stroke:#333
style C3 fill:#ccc,stroke:#333
style P0 fill:#f96
style P1 fill:#f96
style P2 fill:#f96
style P3 fill:#f96
style P4 fill:#f96
style P5 fill:#9cf
end
subgraph "Rebalance 中 - 暂停消费"
direction TB
C1'[Consumer 1] -.->|暂停| P0'[Partition 0]
C1' -.->|暂停| P1'[Partition 1]
C1' -.->|暂停| P2'[Partition 2]
C2'[Consumer 2] -.->|暂停| P3'[Partition 3]
C2' -.->|暂停| P4'[Partition 4]
C3'[Consumer 3] -.->|暂停| P5'[Partition 5]
GC[Group Coordinator] -->|重新分配| ALL
style C1' fill:#f96,stroke:#333,stroke-dasharray: 5 5
style C2' fill:#9cf,stroke:#333,stroke-dasharray: 5 5
style C3' fill:#9f9,stroke:#333,stroke-dasharray: 5 5
style GC fill:#f9f,stroke:#333,stroke-width:2px
end
subgraph "Rebalance 后 - 负载均衡"
direction TB
C1''[Consumer 1] --> P0''[Partition 0]
C1'' --> P1''[Partition 1]
C2''[Consumer 2] --> P2''[Partition 2]
C2'' --> P3''[Partition 3]
C3''[Consumer 3] --> P4''[Partition 4]
C3'' --> P5''[Partition 5]
style C1'' fill:#f96,stroke:#333
style C2'' fill:#9cf,stroke:#333
style C3'' fill:#9f9,stroke:#333
style P0'' fill:#f96
style P1'' fill:#f96
style P2'' fill:#9cf
style P3'' fill:#9cf
style P4'' fill:#9f9
style P5'' fill:#9f9
end
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. 三种策略对比总结
| 特性 |
Range |
RoundRobin |
Sticky |
| 分配粒度 |
按 Topic |
全部分区 |
全部分区 |
| 均匀性 |
一般 |
优秀 |
优秀 |
| 移动成本 |
高 |
高 |
低 |
| 计算复杂度 |
低 |
中 |
高 |
| 适用场景 |
单 Topic |
多 Topic 均匀 |
通用推荐 |
| Rebalance 时间 |
中 |
中 |
短 |
| 消费者增减影响 |
大 |
中 |
小 |
三、触发 Rebalance 的原因
1. 消费者数量变化
graph TD
subgraph "触发场景"
A[消费者数量变化] --> A1[新消费者加入]
A --> A2[消费者主动退出]
A --> A3[消费者崩溃/超时]
A --> A4[消费者取消订阅]
end
subgraph "处理流程"
B[Group Coordinator 检测] --> C[触发 Rebalance]
C --> D[重新分配分区]
D --> E[消费者恢复消费]
end
(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)消费者离开/故障
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 变更
graph LR
A[Group Coordinator 节点故障] --> B[新的 Broker 接管]
B --> C[加载消费者组元数据]
C --> D[触发 Rebalance]
D --> E[所有消费者重新连接]
四、Rebalance 详细流程
1. 完整 Rebalance 时序图
sequenceDiagram
participant C1 as Consumer 1
participant C2 as Consumer 2
participant C3 as Consumer 3
participant GC as Group Coordinator
participant ZK as Metadata Store
Note over C1,C3: 正常消费阶段
C3->>GC: 心跳超时/离开
GC->>GC: 检测到消费者变更
Note over GC: 触发 Rebalance
GC->>C1: 心跳响应: REBALANCE_IN_PROGRESS
GC->>C2: 心跳响应: REBALANCE_IN_PROGRESS
par JoinGroup 阶段
C1->>GC: JoinGroup (选举 Leader)
C2->>GC: JoinGroup (作为 Follower)
end
GC->>C1: 成为 Leader (包含成员列表)
Note over C1: Leader 执行分区分配
C1->>GC: SyncGroup (上传分配方案)
GC->>C2: SyncGroup (下发分配方案)
GC->>C3: 连接超时,踢出组
par 新分配生效
C1->>GC: 提交新 offset
C2->>GC: 提交新 offset
end
Note over C1,C2: 恢复正常消费
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
九、总结
Rebalance 核心要点
| 维度 |
关键点 |
| 三种策略 |
Range(范围)、RoundRobin(轮询)、Sticky(粘性) |
| 触发原因 |
消费者变化、分区变化、订阅变化、Coordinator变更 |
| 主要缺点 |
Stop-The-World、重复消费、延迟增加 |
| 优化方向 |
Sticky策略、参数调优、静态成员、幂等处理 |
| 监控重点 |
Rebalance频率、耗时、Lag变化 |
Rebalance 是 Kafka 实现自动负载均衡的核心机制,但也是一把双刃剑------它保证了高可用和弹性,但也带来了短暂的服务暂停和重复消费。通过选择合适的分配策略、合理配置参数、实现幂等处理和状态管理,可以将 Rebalance 的影响降到最低。