什么是Rebalance
为了提升消费效率,kafka会将Topic消息放到多个Partition,消费者Consumer可以以消费组(Consumer Group)的形式进行并行消费
在这个背景下,每个消费者节点负责一个或多个Partition。为了保证负载均衡,多个Partition能均匀分配给消费节点,Kafka需要会根据节点负载进行Rebalance以及维护这个映射
- 为了提升单个Topic的并发性能,将单个Topic拆分为多个Partition
- 为了提升横向扩展能力,将多个Partition分布在多个不同的Broker
- 为了提升可用性,为每个Partition加了多个副本
- 为了协调和管理kafka集群的数据消息,引入Zookeeper作为协调节点
Reblance过程不是瞬间完成的,而是要经历:注册旧分区👉选举Leader👉分配新分区👉初始化消费者 这一完整过程。在这个期间,所有消费者会停止消费
什么时候会触发Rebalance
[最常见]消费者数量发生变化(服务扩缩容的时候、某个节点出问题了重复重启)- 调整了Topic的分区数量(kafka只支持增加),新增的分区需要分配消费者
- 相同消费组下订阅的Topic列表发生了变化
[隐藏的坑]消费者心跳超过了配置的session.timeout.ms(通常是45s)没有发,kafka会把这个消费者判定为死亡,从而踢出[隐藏的坑]消费超时:通常单批消息超过max.poll.interval.ms(通常是5分钟)没有处理完(没有给ack),kafka会把这个消费者踢出
Rebalance期间会引发哪些问题
- 上述提到,Reblance过程不是瞬间完成的,且这个期间,所有消费者会停止消费,所以会产生消息积压
所以产生积压的时候,优先看下是否发生了Rebalance
- 消息重复和丢失(见下方)
- Rebalance 要靠Coordinator协调,频繁触发会占用Kafka集群的CPU和网络资源
- 而且Kafka默认的分区分配策略(Range 或 RoundRobin),很容易导致负载不均
- 比如5个分区分配给2个消费者,可能出现 3个分区 vs 2个分区 的情况,其中一个消费者压力翻倍,处理速度变慢,又会触发新的Rebalance,陷入恶性循环
什么时候会丢消息
通常情况下,Reblance不会直接导致丢消息。消费者的处理和offset提交顺序没处理好,就容易出现丢消息的现象
- 消费者先提交offset(比如100),然后处理消息
- 开启了自动提交offset(比如100),也是没处理完就提交offset了
上述两种情况,当消费者在处理过程中断了(宕机、OOM、被kill等),导致Kafka触发Rebalance,其它节点接管这个Partition的消费,会从101开始消费
sequenceDiagram participant A as Consumer A participant K as Kafka Broker participant B as Consumer B Note over A,K: 场景:自动提交 (Auto Commit) A->>K: Poll 消息 (Offset 100-200) activate A Note right of A: 放入本地队列/线程池 Note over A,K: 触发自动提交 (5秒到了) A-->>K: 自动提交 Offset = 200 Note right of K: 记录消费进度 = 200 A->>A: 正在处理 Offset 150... Note right of A: 💥 节点突然宕机 (150-199 未处理完) deactivate A Note over K: 心跳丢失 -> Rebalance K->>B: 分配分区给 B B->>K: 获取已提交 Offset K-->>B: 返回 Offset = 200 B->>K: Poll 下一批消息 (201+) Note right of B: 😱 Offset 150-199 永久丢失
正确的做法应该是先处理消息→再提交 offset,确保消息处理完才更新进度。
什么时候会重复消费
kafka Rebalance导致的重复消费更普遍,核心原因是offset提交晚于消息处理。
- 开启手动提交的情况下,若在处理完消息→提交 offset 的间隙触发 Rebalance,offset 没提交成功,新消费者会从上次提交的位置重新消费
sequenceDiagram participant A as Consumer A participant K as Kafka Broker/Coordinator participant B as Consumer B Note over A,K: 初始状态:上次提交 Offset = 99 A->>K: Poll消息 (Offset 100-200) A->>A: 业务处理消息 (100-200) Note right of A: 业务处理完毕,准备提交Offset K-->>A: 心跳超时/Session超时 Note over K: 判定A死亡,触发Rebalance K->>B: 分配分区给 B B->>K: 获取已提交Offset K-->>B: 返回 Offset = 99 (A没来得及提交) B->>K: Poll消息 (Offset 100-200) Note right of B: 重复消费了 100-200 - 处理消息耗时超过 max.poll.interval.ms,消费者被判定死亡,但实际还在处理消息。
sequenceDiagram participant A as Consumer A participant K as Kafka Broker/Coordinator participant B as Consumer B A->>K: Poll 这一批消息 activate A Note right of A: 开始处理 (耗时较长) Note over A,K: 经过 5 分钟 (max.poll.interval.ms) Note over K: A 迟迟没发起下一次Poll<br/>判定A死亡,触发Rebalance K->>B: 分配分区给 B B->>K: 获取 Offset (旧值) activate B Note right of B: B 开始重复处理这批消息 Note over A: 经过 6 分钟,A 终于处理完了 A->>K: 尝试 Commit Offset K-->>A: 拒绝提交 (Fenced / Not Group Member) deactivate A deactivate B - 如果消费者组的auto.offset.reset设为earliest(默认是 latest),Rebalance后找不到已提交的offset(比如offset数据损坏),会从Topic最早的消息开始消费,导致历史消息重复。
如何减少Rebalance以及带来的影响
- 相关参数优化
- 调大消息处理耗时参数
- 调大心跳判定时长,减少消费者被误判下线的几率
- 选粘性分配策略:把partition.assignment.strategy设为StickyAssignor,Rebalance时尽量保留原有分配,减少分区变动。
- 优先手动提交,关闭自动提交(enable.auto.commit=false),在消息处理完成后再提交;
- 必要时用事务,如果业务不允许重复消费,结合 Kafka 事务,确保消息处理 和 offset 提交原子性。
- 消息体内设置唯一字段,消费端根据此字段做好幂等处理