一次「扩容引发的停摆」
老王给 Kafka 消费组加了一台机器,预期消费能力翻倍,结果监控上消费 TPS 直接归零了一分多钟,还冒出一堆重复消费。这就是Rebalance(再均衡):分区在组内成员之间重新分配的过程。这一篇讲清楚它何时发生、为什么慢、怎么治。
分区怎么分:分配策略的演进
Rebalance 的核心动作是把 partition 分给组里的 consumer。策略有几代:
- RangeAssignor:按 Topic 逐个切分分配,缺点是排前面的消费者总多拿一点,负载略偏;
- RoundRobinAssignor:轮询均分,比 Range 均衡,但 Rebalance 时几乎全组分区的归属都会变动;
- StickyAssignor:在均衡的前提下,尽量保留每个消费者原有的分区——减少无谓的分区搬家;
- CooperativeStickyAssignor(2.4+):增量协作式 Rebalance,只收回真正需要迁移的分区,其余消费者边干活边迁移,把全组停摆变成局部停摆。新版本默认首选。
何时触发:四类导火索
- 组内成员增减:扩容、缩容、消费者崩溃或优雅退出;
- 心跳超时(session.timeout.ms 内没收到心跳):消费者被判定死亡;
- 两次 poll 间隔超时(max.poll.interval.ms):一批消息处理太久,被认为卡死;
- 订阅变化:Topic 数量或正则订阅匹配结果变了。
生产的 Rebalance 风暴,八成是后两类「假死」触发的:消费者活得好好的,只是 GC 停顿太久没发心跳,或者单批数据处理超过了 max.poll.interval.ms——Kafka 误判它死了,于是全组重分,重分期间整组停摆(Eager 协议),重分完位点回退又开始重复消费。一个故障触发一轮 Rebalance,Rebalance 又引发停摆和重复,雪球就这么滚起来了。
治风三招
第一招:参数配比。心跳要快于超时的一半:session.timeout.ms=45s 时,heartbeat.interval.ms 设 3s,留足误判余量。max.poll.interval.ms 要大于最慢一批的处理时间,同时调小 max.poll.records(默认 500),让每批处理时间可控——处理能力不够靠加消费者,而不是靠一次拉一大堆。
第二招:静态成员。给每个消费者配置 group.instance.id(静态成员标识)。重启不换 ID,Broker 认出它是老朋友,常规发版重启不再触发 Rebalance——这个参数治好了半个行业的停摆。
第三招:换协作协议。启用 CooperativeStickyAssignor,把全组 STW 缩成受影响分区的局部暂停。升级要整组版本一致,灰度时注意。
offset 的管理与提交
消费进度(offset)记在 Broker 端的内部主题 __consumer_offsets 里。提交 API 两种:commitSync(同步,可靠但阻塞)与 commitAsync(异步,快但可能丢提交)。工程惯例是两者混用:平时异步提交保持吞吐,消费循环结束或关闭前同步提交一次保底。再加一条第 6 篇的铁律:先处理完业务再提交,宁可重复不可丢失。Rebalance 监听器(ConsumerRebalanceListener)里记得在分区被收回前做最后一次同步提交,能少一批重复。
排查清单
| 症状 | 排查方向 |
|---|---|
| 频繁 Rebalance | 消费者 GC 日志、心跳线程、max.poll.interval 是否被慢 SQL/慢 RPC 拖爆 |
| Rebalance 后大量重复 | 提交间隔太长,位点回退窗口大;检查监听器是否补提交 |
| 发版必停摆 | 没用静态成员或滚动发版顺序不对 |
| 部分分区长期无消费 | 消费者数多于分区数,多出来的在空转 |
小结
Rebalance 本身不是故障,是必要机制;故障的是被误判触发的频繁全量 Rebalance。三招治风:参数配比防误判、静态成员防发版、协作协议降爆炸半径。配比口诀:心跳不过会话半,批处理不过间隔半。
Kafka 两篇完结。下一篇回到 RocketMQ:NameServer 的无状态路由、主从与 Dledger 的自动切换——老王的生产集群到底怎么搭。
评论 (0)