什么是 Kafka 的重平衡机制?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...,查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 基础掌握度:光知道重平衡这个词还不够,面试官更想看你能不能讲清楚它的触发条件、执行流程,还有它在消费者组模型里扮演的角色。

  2. 原理深度:重平衡背后有一套完整的协调机制:Group Coordinator、JoinGroup、SyncGroup,还有 Leader 消费者制定分配方案的设计。能不能讲到这一层,是 “用过 Kafka” 和 “懂 Kafka” 的分水岭。

  3. 生产实践意识:重平衡是消费端最大的性能杀手之一。面试官很想知道:你有没有在实际项目里遇到过重平衡风暴?知不知道怎么通过参数调优、静态成员关系来减少重平衡?答得出这些,基本就是加分项。

核心答案

先说结论:重平衡(Rebalance)是指消费者组内的分区所有权重新分配的过程。当消费者成员发生变化、订阅的主题调整,或者主题分区数增减时,Kafka 会把分区重新分配给组内的消费者,保证每个分区始终只被组内的一个消费者消费。

整个重平衡由两方协作完成:

角色 所在位置 职责
Group Coordinator Broker 端 感知成员变化,发起重平衡,下发分配方案
消费者(含 Leader 消费者) 客户端 普通成员参与投票;Leader 成员制定分区分配方案

重平衡最大的问题是:执行期间整个消费者组停止消费,也就是所谓的 STW。所以生产环境优化的核心思路就一句话——能不重平衡,就别重平衡。

深度解析

一、先搞清楚背景:分区独占消费模型

消费者组有一套铁律:一个分区在同一时刻,只能被组内的一个消费者消费;反过来,一个消费者可以消费多个分区。重平衡维护的正是这条 “分区 → 消费者” 的映射关系。

Kafka 消费组分区分配
Kafka 消费组分区分配

上图就是一个典型的消费模型:4 个分区被 2 个消费者瓜分,每人两个。一旦这个映射关系需要打破重来——比如消费者 B 挂了,或者组里新进来一个消费者 C——重平衡就该登场了。

Kafka 重平衡分区分配
Kafka 重平衡分区分配

二、重平衡的触发条件

能触发重平衡的事件只有三类:

  • 消费者组成员变化:最常见的来源。新消费者加入、旧消费者主动退出、消费者被 Group Coordinator 判定 “已死” 踢出组,都会触发。线上大多数重平衡事故都出在这一类。
  • 订阅主题数变化:用正则表达式订阅主题时(比如 subscribe(Pattern.compile("order-.*"))),新匹配到的主题的分区会触发重平衡。固定主题名订阅一般不会。
  • 主题分区数变化:给已有主题增加分区,新分区没有归属,必须重平衡。

这里有个很容易踩的坑:消费者 “卡死” 也会被当成成员变化。业务处理逻辑太重、一次 poll() 拉回来的消息处理太久,超过 max.poll.interval.ms,协调者就认为这个消费者挂了,把它踢出组,触发一轮重平衡。很多人线上反复重平衡查不出原因,最后发现是消费逻辑太慢。

Kafka poll 超时重平衡
Kafka poll 超时重平衡

三、重平衡的完整流程(面试核心)

经典协议(也就是现在大多数 Kafka 版本在用的协议)的重平衡分两大步:JoinGroup 阶段 和 SyncGroup 阶段,外加前置的一步 FindCoordinator。

Kafka 重平衡流程
Kafka 重平衡流程

对照上图,把几个关键点掰开说:

  • 第一步:找协调者。每个消费者组对应一个 Group Coordinator,它是 Broker 端的协调者,负责管理这个组的成员和重平衡。消费者启动后先发 FindCoordinator 请求找到它。具体落在哪个 Broker 上,由 __consumer_offsets 主题对应分区的 Leader 副本所在节点决定,老版本对应关系是取 hash(groupId) % 50(offsets.topic.num.partitions 默认 50)。
  • 第二步:JoinGroup,选 Leader。所有消费者向协调者发送 JoinGroup 请求,带上自己的订阅信息。协调者等大家都到齐了,从中选出一个成员担任 Leader 消费者(通常是第一个发送 JoinGroup 的成员)。注意,这个 Leader 是客户端侧的消费者,不是 Broker。只有 Leader 能在响应里拿到完整的成员列表和订阅信息。
  • 第三步:SyncGroup,制定并下发分配方案。Leader 根据成员列表和订阅信息,按分配策略算出每个消费者该拿哪些分区,把方案塞进 SyncGroup 请求发给协调者;其他消费者也发 SyncGroup,但内容为空,纯粹是等结果。协调者收到方案后,把各自的那份下发给每个成员。
  • 第四步:恢复消费。所有消费者拿到自己的分区后,从上次提交的位移处继续消费,重平衡结束。

这套设计有个耐人寻味的地方:分配逻辑放在客户端(Leader 消费者)执行,而不是 Broker 执行。好处是分配策略可以灵活定制,客户端升级就能换策略,Broker 不用跟着改。坏处也明显——所有消费者得停下来等 Leader 算方案、等协调者下发,这段时间整个组都在空转。

还有一点必须强调:重平衡期间,组内所有消费者都要释放自己手里的分区,停止消费。不是只调整变化的那部分,而是全部推倒重来(经典协议下确实如此)。分区数多、消费者多的时候,一次重平衡可能持续几分钟,这期间消息堆积蹭蹭往上涨。

Kafka 重平衡协调分工
Kafka 重平衡协调分工

四、分区分配策略

Leader 消费者制定方案时,按哪个算法分?这就是分配策略,Kafka 内置了 4 种:

策略 类名 思路 问题
范围分配(默认) RangeAssignor 按主题逐个分配,分区按序号范围切给消费者 多个主题下,前面的消费者总会多拿分区,容易倾斜
轮询分配 RoundRobinAssignor 所有主题的分区放一起,逐个轮询分发 分配更均匀,但代价是动静大
粘性分配 StickyAssignor 尽量保留上次分配结果,只调整必要的部分 减少分区在消费者间搬运
协作粘性分配 CooperativeStickyAssignor 在粘性基础上支持增量重平衡(Kafka 2.4+) 需要客户端 2.4 以上版本

前三种属于 Eager(饥饿式)协议:重平衡一开始,所有消费者无条件撤销全部分区,等新方案出来再重新分配,这就是 STW 的根源。第四种 CooperativeStickyAssignor 是增量式的,下面单独说。

五、重平衡的危害

为什么大家谈重平衡色变?主要是三板斧:

  • STW,全组停摆:重平衡期间整个消费者组没法消费消息,分区越多、成员越多,一次拖得越久,代价越惨。
  • 重复消费:重平衡发生时,如果消费者还没来得及提交位移就被撤销了分区,新接手的消费者会从上次提交的位置开始消费,中间那段就会被消费两次。
  • 连锁反应:上游消费卡住 → 消息堆积 → 下游任务延迟,如果业务上有超时兜底逻辑,一轮重平衡风暴能引发一串告警。

六、如何减少重平衡(生产实践重点)

知道了触发条件,规避思路也就清楚了:让协调者别误判消费者 “死了”。核心参数就这几个。

参数 默认值 作用 调优建议
session.timeout.ms 45s(3.0 之前是 10s,KIP-735 调整) 会话超时时间,超过没心跳就判死 不要设太短,网络抖一下就误杀
heartbeat.interval.ms 3s 心跳间隔 一般设为 session.timeout.ms 的 1/3
max.poll.interval.ms 5min 两次 poll() 的最大间隔 业务处理慢就调大,这是 “被踢群” 的头号原因
max.poll.records 500 单次 poll() 拉取的最大条数 处理慢就调小,让 poll() 更频繁

除了调参数,还有两个更治本的招:

1. 静态成员资格(Kafka 2.3+)

给每个消费者配置一个固定的 group.instance.id,重启后协调者认得它是 “老熟人”,不触发重平衡。这对滚动发布场景特别友好——发布重启不再引发组内风暴。

2. 增量协作式重平衡(Kafka 2.4+)

把分配策略换成 CooperativeStickyAssignor,重平衡不再 “全部回收重来”,而是只回收需要迁移的分区,其他分区照常消费。两次小协商就能完成,把 STW 的范围缩到了最小。新项目建议直接上这个。

Kafka 重平衡协议对比
Kafka 重平衡协议对比

七、一提新版本:KIP-848

经典协议的 JoinGroup/SyncGroup 机制被诟病多年,Kafka 社区在 2.4 引入增量式协作重平衡之后,又在 KIP-848 里拿出了换代方案 全新的消费者组协议:消费者不再推举 Leader,也不用客户端算分配方案,直接向 Broker 端的组协调者上报订阅信息,由服务端单方面分配。重平衡期间消费者也不用停消费。这套协议在 Kafka 3.7 进入早期预览,4.0 已经正式转正(GA)。面试里提到 KIP-848,绝对是区分度拉满的加分项。

面试高频追问

  1. 追问一:重平衡期间消费者还能消费消息吗?

    经典协议(Eager)下不能,所有消费者撤销分区、暂停消费,直到新方案下发。这也是重平衡最大的痛点。Kafka 2.4 的 CooperativeStickyAssignor 支持增量式,只撤销要迁移的分区,其余不受影响。

  2. 追问二:消费者被踢出组的判定条件是什么?

    两条:一是 session.timeout.ms 内没收到心跳,协调者认为它挂了;二是两次 poll() 间隔超过 max.poll.interval.ms,消费者主动离组。这两种情况都会触发重平衡。

  3. 追问三:__consumer_offsets 和重平衡有什么关系?

    __consumer_offsets 是存储消费位移的内部主题,每个消费者组的 Group Coordinator 就落在该主题某个分区的 Leader 副本所在的 Broker 上,所以它和协调者的定位有关。同时位移提交失败(比如重平衡进行中提交)还会返回 REBALANCE_IN_PROGRESS 异常。

  4. 追问四:怎么排查线上频繁重平衡的问题?

    先看日志里的 Attempt to heartbeat failed、Rebalance 关键字,再对时间点,区分是成员真变了还是误判。误判就查 max.poll.interval.ms 是否太小、心跳线程是否被长时间 GC 打断、网络是否抖动。最后考虑静态成员资格 + 协作式分配。

常见面试变体

  • 变体一:Kafka 重平衡的触发条件有哪些?
  • 变体二:Kafka 消费者组的分配策略有哪几种?
  • 变体三:如何避免 Kafka 频繁重平衡?
  • 变体四:Kafka 2.4 的增量协作式重平衡和经典重平衡有什么区别?

记忆口诀

触发条件:成员变、主题变、分区变。流程四步:找协调者 → 入组选 Leader → Leader 出方案 → 协调者发方案。规避三招:心跳别太紧、拉取别太狠、上静态成员。

总结

一句话:重平衡是消费者组在成员或分区变化时重新分配分区的自愈机制,由 Group Coordinator 和 Leader 消费者通过 JoinGroup/SyncGroup 两阶段协作完成。它换来的是消费组的弹性,但代价是全组 STW,生产环境要用好 max.poll.interval.ms 等参数、静态成员资格和协作式分配,把不必要的重平衡摁住。