前阵子凌晨三点被值班电话叫醒,告警信息写得很简短:某个核心 topic 的分区持续报NotLeaderForPartitionException,生产端大量重试。我登录集群一查,Controller 日志在不断切换,ZooKeeper 连接时断时续,所有分区像失去了主心骨一样轮流抖动。那一夜我把 Kafka 的选举机制从头到尾过了一遍,也把曾经“知道大概”的很多细节彻底吃透了。这篇文章就想把 Kafka 的选举过程掰开揉碎讲清楚:集群里到底有哪几种选举、Controller 是怎么选出来的、分区 Leader 是怎么定的、选错 Leader 会付出什么代价,以及 KRaft 模式到来之后这些规则又发生了什么变化。适合正在维护 Kafka 生产环境、或者准备深入理解 Kafka 内部机制的同学,希望你看完能少走我踩过的那些弯路。
1. 先把“选举”这个词拆开:Kafka 里其实有三套选举逻辑
1.1 三种完全不同的选举,别混为一谈
很多人一说“Kafka 的选举”,第一反应就是“Kafka 用 ZooKeeper 选 Leader”。这句话只说对了一小半。Kafka 内部至少存在三种和“选举”相关的机制,它们的作用范围、实现方式、依赖组件完全不同。
第一种是Controller(控制器)选举。整个 Kafka 集群里只有一个 Controller,它负责管理分区状态、分区 Leader 选举、Replica 分配、topic 创建删除这类元数据操作。在旧架构里,Controller 是通过竞争 ZooKeeper 的临时节点选出来的。它选出来后,才会接着去处理下面第二种选举。
第二种是分区 Leader 选举。每个 topic 分区有多个副本(Replica),读写请求只能打在 Leader 副本上,Followers 从 Leader 同步数据。当 Leader 副本所在的 Broker 宕机,或者副本状态发生特殊变化时,需要重新在副本集合里挑一个新 Leader。这项工作的执行者是 Controller,具体规则则是“从 ISR 集合里按顺序挑一个可用的副本”。这是真正决定读写路径命脉的一层选举,也是生产事故中最常被问责的环节。
第三种是消费组里的组 Leader。这个词其实有点名不副实,它并不是什么高可用选举,更像是第一个向 GroupCoordinator 注册的 Consumer 自动成为 group leader,负责给组内成员分发分区分配策略。如果这个 Consumer 退出,其他成员会重新触发一轮 join group,再产生一个新的组 Leader。这套逻辑和 ZooKeeper、Raft 没有任何关系,也不需要从多个候选者里“竞争上岗”。
1.2 为什么同一个系统里要叠三套选举机制
说到底,这是由 Kafka 的分层架构决定的。上面三层各自保护不同类型的状态:
- Controller 保护的是全局元数据,比如“谁有权修改分区规划”“下一次分配的分区 ID 从哪里开始”。这种状态只能有一个权威,必须用强一致性的外部组件(ZooKeeper 或 KRaft 里的 Raft 日志)来仲裁。
- 分区 Leader 保护的是单分区的读写归属,它不要求所有分区在同一时刻都完成重选,只需要在 Controller 的统一调度下逐批收敛。
- 消费组 Leader 保护的只是组成员视图和分区分配策略的传播,这里丢失了状态可以重算,所以根本不搞真正意义上的持久化选举。
如果一开始不理解这个分层,后面看 Controller 选举和分区选举时会经常觉得混乱:明明 Controller 已经选完了,为什么分区还是没有 Leader?其实是因为 Controller 选举只是“产生了裁判”,分区选举是“裁判开始干活”。二者之间有先后关系,也有依赖关系。
1.3 看这篇文章需要先建立的组件印象
后续内容会反复提到几个名词,先简单铺垫一下:Broker 是 Kafka 的服务器进程,每个 Broker 有自己的唯一 ID;Topic 是一类消息的逻辑集合,Topic 被切成若干个分区;每个分区有 1 个 Leader 副本和 N 个 Follower 副本;ZooKeeper 在旧架构里负责保存集群元数据和提供分布式协调原语,KRaft 模式下则被一套内部元数据日志取代。有了这些基础再看选举过程,就不会被细节绕晕。
2. Controller 选举:抢一个临时节点,胜者成为集群大脑
2.1 临时节点的游戏规则:谁创建成功,谁就是 Controller
在 ZooKeeper 模式下,Controller 选举其实比很多人想象的简单。每个 Broker 启动时都会尝试在 ZooKeeper 的/controller路径下创建一个类型为EPHEMERAL的临时节点,节点内容就是自己的 Broker ID。ZooKeeper 的节点创建是原子操作,路径唯一的特性保证最终只有一个 Broker 能创建成功。创建成功的那一个,就成为集群 Controller。
这里有两个关键点决定了它的可靠性。
第一,临时节点和 Broker 与 ZooKeeper 之间的会话绑定在一起。如果 Broker 进程崩溃,或者它和 ZooKeeper 之间的会话因为超时而失效,ZooKeeper 会自动删除这个临时节点。只要节点还在,就说明当前 Controller 认为自己还活着、还有资格管理集群。
第二,所有没抢到节点的 Broker 并不会真正“落选”,它们会在该节点上设置一个 Watch。一旦节点消失,所有 Watcher 都会收到通知,然后重新发起创建竞争。由于创建是原子操作,下一轮又只会产生一个胜者。整个过程本质上就是“抢锁”,谁拿到锁谁当 Controller,拿不到就等锁释放再抢。
2.2 Controller 切换时的完整处理链路
光是抢到临时节点还不够,新 Controller 走马上任后要做一系列初始化,否则就是光杆司令。完整链路大致如下:
- Broker 启动,向 ZooKeeper 注册自身信息后,尝试创建
/controller临时节点。 - 创建成功的 Broker 从 ZooKeeper 读取
/controller_epoch节点,并把它加 1 后写回,得到一个新的 Controller Epoch。 - 新 Controller 读取所有 Broker 列表、所有 Topic 的分区分配关系、所有分区的当前 Leader/ISR 状态。
- 它对所有 Broker 发送
LeaderAndIsrRequest,要求它们更新自己管理的分区的 Leader/ISR 信息。 - 如果旧 Controller 挂掉时还有未完成的分区选举,新 Controller 会继续接管这些决策,把分区状态机推到收敛状态。
这里有个非常重要的概念叫Controller Epoch。它是一个单调递增的整数,用来标识 Controller 的任期。每次 Controller 变更,Epoch 都会加一。Broker 在处理来自 Controller 的请求时,会检查请求里的 Epoch 是否小于本地保存的 Epoch。如果更小,就直接拒绝。
为什么要多此一举?因为分布式系统里有一个著名的“脑裂”问题。比如某台 Broker 和 ZooKeeper 之间的网络出现了分区,导致临时节点被 ZooKeeper 删掉,另一个 Broker 抢到了节点成为新 Controller。但此时旧 Controller 进程还没死,它可能还认为自己仍然是 Controller,还在继续向集群发号施令。如果两套指令都生效,分区分配、Leader 迁移这些操作就会互相打架。Controller Epoch 就是用来区分新旧权威的令牌,旧 Controller 发出的所有请求都会因为携带旧 Epoch 而被 Broker 拒之门外。
2.3 为什么 Controller 的可用性直接决定了整个集群的稳定性
有段时间我怀疑过:Controller 只有一个,岂不是单点?答案是在旧架构里确实是单点,只不过它是一套“可以快速自动重新选举的单点”。Controller 本身不承担数据读写,它在大多数时间里并不繁忙,但一旦它不可用,整个集群就会进入一种“元数据冻结”状态:
- 新 Topic 无法创建,已有 Topic 的分区无法扩容。
- Broker 上下线时,分区的重选举和重分配没有办法推进。
- 分区 Leader 挂了之后,Controller 无法及时为它指定新 Leader,该分区就会一直处于无 Leader 状态,生产和消费全部报错。
我经历的那次故障之所以严重,就是因为 Controller 在反复切换:旧 Controller 的会话不稳定,刚当选的新 Controller 又因为建立完整元数据视图时依赖 ZooKeeper 读取,而 ZooKeeper 本身连接抖动,导致它迟迟没有完成初始化。Controller 一直处于“选出来但没干成正事”的状态,分区自然全乱套了。
所以,排除问题时的第一直觉应该是:先确认 Controller 有没有稳定当选,再看分区层是否在等待指令。如果 Controller 自身还在抖动,而且 ZooKeeper 节点健康状态不佳,再往上排查网络、磁盘 IO、JVM GC 等情况才是正路。很多人一看到分区无 Leader 就去调unclean.leader.election.enable,结果 Controller 都没稳定,调了也白调。
2.4 Controller 和分区选举的关系:先把位置摆正
Controller 当选后,分区 Leader 选举这件事的主体就是它。分区选举不是每个 Broker 自己私下决定的,而是由 Controller 统一计算、统一下发指令。这就意味着,Controller 的稳定性是分区选举能否及时完成的大前提。
我常跟同事打比方:Controller 是交警总队,分区 Leader 是每个路口的执勤交警。路口交警撤了,需要总队派新人顶上。如果总队本身乱了,各路口就只能瘫痪。所以看 Kafka 选举,一定要先看总队,再看路口。接下来我们就走进路口这一层,看看分区 Leader 具体是怎么定的。
3. 分区 Leader 选举:读写路径的生死门
3.1 从 AR、ISR、OSR 三个集合说起
要理解分区选举,必须先理解副本状态的三种集合。
AR(Assigned Replica)是一个分区的全部副本列表,也就是创建 Topic 时分配的副本列表,顺序很重要。ISR(In-Sync Replica)是与 Leader 保持同步的一组副本,只有同步中的副本才有资格被选为新 Leader。OSR(Out-of-Sync Replica)是同步落后甚至完全断开的副本集合,理论上属于 AR 但不属于 ISR 的部分就是 OSR。
Follower 不是一直在 ISR 里的。它们会持续从 Leader 拉取消息,如果某个 Follower 在replica.lag.time.max.ms时间内没有跟上 Leader 的最新消息,或者长时间没有发起拉取请求,Leader 会把它从 ISR 中移除。被移出 ISR 后,它没有资格参与下一轮选举。这个设计非常合理:一个已经落后十万八千里的副本,如果被选为 Leader,那么它“不知道”的消息就会被当作不存在,相当于直接丢数据。ISR 机制从源头上尽量避免这种局面。
Leader 本身也会维护一份 ISR 列表,并且把 ISR 的变更同步给 Controller,再写入 ZooKeeper 的分区状态节点。这样就算 Leader 宕机,Controller 也能拿到最近一次的 ISR 快照。
3.2 触发分区 Leader 选举的常见场景
分区选举不是只有“Leader 宕机”这一种触发条件,至少包括下面几类常见场景:
- Broker 宕机:Leader 所在 Broker 进程退出,或者和 Controller 之间的心跳长时间中断,Controller 判定该 Broker 失效,对受影响分区触发重选。
- 优雅关停(clean shutdown):Broker 正常下线时,会通知 Controller 自己要关闭,Controller 可以提前把这些 Broker 上的 Leader 角色迁移到其他副本,避免写入中断。
- Preferred Leader 选举:Kafka 允许每个分区指定一个 Preferred Replica(通常是 AR 列表里的第一个副本)。当这个副本重新追上 ISR 后,Controller 可以触发一轮“回归”选举,让分区恢复到理想分布,这是负载均衡的重要手段。
- 分区重分配:对 Topic 执行
kafka-reassign-partitions.sh之后,新副本追平数据、加入 ISR 后,会触发 Leader 切换到目标副本。 - 副本数据截断后重新上线:某些副本日志损坏或落后过多,恢复后需要通过选举重新确定它们跟随谁。
3.3 选举规则的两条线:ISR 优先,以及“脏选举”的代价
Controller 执行分区选举时,核心逻辑是从该分区的 ISR 列表中挑一个副本作为新 Leader。具体选择方式比较简单:在 ISR 列表里按顺序寻找第一个可用副本,如果可用,它就成为新 Leader。ISR 列表的顺序其实就是副本加入同步的顺序,通常情况下也能代表数据完整程度。
如果 ISR 里恰好有一个可用副本,选举会非常顺利,新 Leader 和旧 Leader 的数据差距很小,Follower 只需要补齐最后一段日志就能继续服务。
但真正难处理的是 ISR 为空的情况。比如一台机器同时挂了多个副本,或者一个副本落后太多被踢出 ISR 后机器又宕机,ISR 列表可能变成空。此时 Controller 面前只有两个选择:
第一个选择是什么都不选,继续等待 ISR 里某个副本恢复可用。这个过程可能要持续很久,分区在此期间处于无 Leader 状态,生产和消费双停。优点是只要恢复的副本在线,就没有数据丢失风险,所有副本重新对齐后继续工作。
第二个选择是开启unclean leader election(脏选举)。允许 Controller 从 AR 列表里选一个不在 ISR 里的副本当 Leader,哪怕它已经落后很多。它的优点是分区能在最短时间内恢复写入,代价是从这个新 Leader 视角看,它没同步到的消息全部丢失。更严重的是,旧 Leader 如果稍后重新上线,发现自己变成了 Follower,会按照新 Leader 的日志偏移截断自己多余的数据。这批数据在任何副本里都再也找不回来。
记得一个真实教训:某业务团队为了追求“写入永远可用”,在生产环境把unclean.leader.election.enable设成了 true,结果一次抖动中 ISR 恰好只剩一个落后副本,系统自动把它选成了 Leader,几千条还没被下游消费的消息就这样凭空消失。虽然消息系统本来就不该被当作数据库来用,但那次事故让我彻底意识到,脏选举这个开关是在用数据完整性换写入可用性,必须业务形态允许丢数据才敢开。
3.4 Preferred Leader 与min.insync.replicas的连带关系
还有一个容易被忽视的点:分区选举完成不代表整个集群的负载就均衡了。默认情况下,每次重选只是随机地从 ISR 里挑副本,可能挑来挑去都聚集在某台机器上。所以 Kafka 设计了 Preferred Replica 机制,在 Broker 正常启动或分区重新分配后,自动把分区 Leader 迁回 AR 列表的第一个副本。
对于写多读少的业务,我把auto.leader.rebalance.enable保持默认开启,让集群自动做 Leader 均衡;对于某些延迟敏感场景,手动执行kafka-leader-election.sh --election-type preferred在低谷期操作,会比自动均衡更容易控制窗口。
另外,很多人只知道min.insync.replicas和写入可用性有关,不知道它也会影响选举后的真实服务能力。假设min.insync.replicas=2,分区原本有 3 个副本,其中一个宕机触发了重选,新 Leader 在 ISR 里只剩一个可用副本。此时生产者写入依然可以成功,但因为没有达到最小同步副本数,写入开始报NotEnoughReplicasException。业务方如果对这类错误不敏感,会以为新 Leader 选举成功后一切正常,实际上可用性窗口已经悄悄缩水了。所以检查选举结果时,一定要连带看新 ISR 的数量是否满足min.insync.replicas,否则表面恢复,实际还是半瘫痪。
4. 那次“分区无 Leader”排障记录:把所有机制串成一条线
4.1 故障现象:写入大面积超时,元数据请求疯狂重试
回到开头那个凌晨事故。当时的具体表现是:某个核心 topic 的 12 个分区里,有 7 个分区处于无 Leader 状态,生产端持续抛出NotLeaderForPartitionException,客户端的元数据请求在不停重试,但返回的响应里始终没有可用 Leader 信息。我去看 Broker 日志,发现多个 Broker 同时打出了“Controller 变更”相关日志,Controller ID 在短短十分钟内换了三四次。
这种“Controller 反复横跳 + 多个分区同时无 Leader”的组合,几乎可以断定问题在 Controller 层,而不是某个单个分区的副本故障。单个分区 Leader 挂了,Controller 会很快指定一个新的;多个分区同时挂,说明 Controller 根本没有完成它该做的调度。
4.2 排查链路:不要上来就开脏选举
我当时的排查顺序是这样的,也建议你照这个顺序走一遍:
- 确认 Zookeeper 集群状态。旧架构下这是第一环。看每个 ZooKeeper 节点是否健康、follower 是否同步、有没有频繁选主。当时发现其中一个 ZooKeeper 节点响应延迟极高,命令执行经常超时。
- 确认 Broker 到 ZooKeeper 的会话是否稳定。在 Broker 日志里搜 Session Expired 相关记录,如果大量出现,就能解释为什么临时节点频繁消失、Controller 频繁切换。
- 确认当前 Controller 是否完成初始化。查看当前有效 Controller 的日志,看它是否成功读取了全量元数据、是否开始下发
LeaderAndIsrRequest。 - 确认具体分区卡在哪个状态。通过命令行工具查看分区详情,无 Leader 的分区会显示
leader: -1,我们要看它的 ISR 是否还有存活副本。
在这期间,我没有去动unclean.leader.election.enable。原因很简单:ISR 里明明还有存货副本,问题只是 Controller 调度没恢复。脏选举只能解决“副本全挂了”的场景,而且代价是丢数据。在这种场景下开脏选举,等于用丢数据换一个本就该恢复的稳定性,非常不划算。
4.3 根因定位和恢复动作
最终定位到的根因有两层:一层是某台 ZooKeeper 所在机器磁盘 IO 被打满,导致集群整体写入延迟升高,多个 Broker 的会话开始超时;另一层是新 Controller 在初始化时需要大批量读取 ZooKeeper 元数据,而它自身所在 Broker 的 GC 停顿又特别长,导致它长时间没完成初始化,期间再次被判定失联,于是又触发下一轮选举。
恢复动作分三步进行。第一步,先把 ZooKeeper 的磁盘压力源处理掉,恢复 ZooKeeper 节点的正常响应。第二步,等待 Controller 完成一次稳定当选,不再横跳。第三步,确认 Controller 初始化完毕后,手动触发了一次分区 Leader 重选,把无 Leader 的分区在短时间内全部收敛。整个恢复过程用了大概二十分钟,其中大半时间都花在等 ZooKeeper 磁盘 IO 回落上。
4.4 从这次事故里总结出的几条维护规则
事后我给自己定了三条规则,后来也成为我们团队处理 Kafka 异常时的默认共识:
- 生产环境一定要持续监控 ZooKeeper 节点的延迟和磁盘 IO,不要只看 Kafka 自身指标。旧架构下 ZooKeeper 是 Kafka 元数据的命门,它的抖动会以十倍百倍的放大效应体现在 Kafka 层。
- Controller 切换不是必须避免,但必须避免“切换后无法完成初始化”。如果频繁切换,优先排查网络和 GC。
- 任何自动开脏选举的诉求,都要走严格评审。默认保持
unclean.leader.election.enable=false,宁可短时间写入不可用,也不要莫名其妙丢数据。
5. KRaft 时代:ZooKeeper 退场之后,选举规则变成什么样
5.1 为什么最终要丢掉 ZooKeeper
ZooKeeper 这套协调方案在工作得当时很可靠,但它给运维带来了多少额外负担,维护过的人都知道:要单独搭集群、要盯着它的磁盘和网络、要处理它和 Kafka 之间的会话风暴。而且 Controller 本身是单点,ZooKeeper 只解决“谁来当 Controller”的问题,没解决“Controller 的元数据变更如何安全持久化”的所有细节。
KRaft 模式(KIP-500 的落地)从根上改变了这个格局。它的思路是:Kafka 不再使用外部 ZooKeeper,而是用内置的一套 Raft 共识协议来同步元数据日志。Controller 也从“单点实例”升级为“一组 Controller 节点组成的 Quorum”。选举问题不再由一个外部系统来回答,而是由 Kafka 自己用 Raft 来决定。
5.2 元数据 Quorum 的角色:Controller 的产生方式变了
KRaft 模式下,Controller Quorum 是一个 Raft 节点组,通常由奇数个节点组成(3 个或 5 个)。这组节点里每个成员都有完整的元数据日志,通过 Raft 日志复制来保证一致性。Raft 里有一个明确的 Leader 概念,共识组中的所有读写都由 Leader 仲裁,其他节点是 Follower。
当一个 Controller Leader 失联,或者心跳超过选举超时时间,Follower 会触发新一轮投票。它们会检查自己的日志任期,向其他节点发起请求,获得多数派(majority)同意后成为新 Leader。这套流程和 ZooKeeper 的“抢临时节点”有本质区别:ZooKeeper 模式是“谁抢到节点谁获胜”,Raft 模式是“谁拿到多数派投票谁获胜”。
Raft 之所以天然适合做这件事,是因为它自带日志复制和提交机制。Controller 的每一次元数据变更都会先写成 Raft 日志,复制到多数派节点并提交后,才对外生效。这就意味着,即使某个 Controller 节点脑裂,它也无法提交新日志,旧 Leader 的过期操作会被任期号有效拦截。
5.3 分区 Leader 选举逻辑其实没有变
这里有个容易混淆的点:KRaft 改变的只是 Controller 的产生方式,而不是分区选举的决策方式。分区 Leader 依然由当前元数据 Quorum 的 Leader(也就是 Controller)来决定,依然遵循 ISR 优先原则、依然受unclean.leader.election.enable控制。
也就是说,前面讲的分区选举那套东西,在 KRaft 模式下基本可以原样理解。你只是在排查时不再需要去看 ZooKeeper,而是去看 Controller Quorum 的健康状态、看它的 Leader 是否稳定、看元数据日志有没有持续复制。
5.4 运维侧需要重新适应的几个点
- 部署形态变了:不再需要单独部署 ZooKeeper 集群,控制器节点可以直接以
KRaft模式启动,集群规模小了不少。 - 心跳和选举的排查对象变了:以前是 ZooKeeper 的会话是否过期,现在是 Controller Quorum 的 Raft 心跳是否稳定、选举超时是否过短。
- 几个关键参数:
controller.quorum.election.timeout.ms控制触发新一轮选举的等待时间,controller.quorum.request.timeout.ms控制节点间请求超时。网络抖动明显的环境,不适合把选举超时调得过小,否则会频繁触发无意义的 Leader 切换。
另外,KRaft 模式还在持续演进,不同版本的兼容性和稳定性差异比较大。如果你所在团队已经有成熟的 ZooKeeper 模式集群,不要急着大版本迁移,先在测试集群里跑几个月的混沌实验再决定。技术选型这种事,稳定性永远比新鲜感优先。
6. 日常巡检我必看的选举相关指标和命令
6.1 判断当前 Controller 是谁、分区状态怎么查
排查过程中最常用的是三组命令,前端时间我给新同学整理了一个速查,这里也放出来。
第一组是查看当前 Controller。在用 ZooKeeper 的情况下,可以查看 ZooKeeper 节点内容:
# 获取当前 controller 的 broker id get /controller如果返回{"brokerid":2,"timestamp":"xxx"},说明 Controller 是 broker 2。KRaft 模式下则可以通过 Kafka 自带的命令查看:
kafka-metadata-quorum.sh --bootstrap-server localhost:9092 describe --status第二组是查看某个分区的 Leader 和 ISR:
kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic your-topic输出里Leader一列如果出现-1,说明当前分区没有 Leader,可能正处于选举等待或选举失败状态;Replicas是全量副本列表,Isr是当前同步副本,需要重点看两者是否一致、ISR 是否少于副本数。
第三组是查看活跃 Controller 变化和 leader 迁移事件,通常直接在 Broker 日志里搜:
grep -i "controller" server.log | tail -n 100 grep -i "leaderAndIsr" server.log | tail -n 1006.2 一张表记住选举相关参数
| 参数 | 默认值(常见版本) | 作用 | 风险提示 |
|---|---|---|---|
unclean.leader.election.enable | false | 是否允许 ISR 为空时选 OSR 副本当 Leader | true 可能丢已提交数据,谨慎开启 |
auto.leader.rebalance.enable | true | 是否自动执行 Preferred Leader 均衡 | 频繁迁移可能引入抖动,敏感环境按需调低 |
min.insync.replicas | 1 | 写入至少需要多少个同步副本 ack | 值越大可用性越低,数据可靠性越高 |
replica.lag.time.max.ms | 30000 | Follower 超过该时间未同步则被移出 ISR | 调太大可能让 ISR 包含落后副本 |
controller.quorum.election.timeout.ms | 按版本配置 | KRaft 下触发新一轮 Raft 选举的等待时间 | 网络抖动多则调大,避免频繁切换 |
controller.quorum.request.timeout.ms | 按版本配置 | KRaft 下节点间请求超时 | 同左 |
有几个参数我会在每次巡检时逐个确认,特别是unclean.leader.election.enable,我会用一键脚本批量扫描所有集群的配置,发现被意外打开立刻拉回来。因为很多时候它不是被 DBA 主动打开的,而是某次变更配置时把模板里某个测试参数带上去了。
6.3 给新人的故障排查 Checklist
如果你刚接手 Kafka 运维,遇到“无 Leader”“Controller 频繁切换”这类故障,我建议按照下面的顺序快速过一遍:
- 先看 Controller 是否稳定:有没有频繁变化,是否完成初始化。
- 再看 ZooKeeper(旧模式)或 Controller Quorum(KRaft)的基础健康指标:磁盘、IO、网络、CPU。
- 然后拉出所有无 Leader 的分区清单,按 topic 聚合,判断是单个分区问题还是全局问题。
- 确认这些分区的 ISR 里还有没有活着的副本,再做下一步决定。
- 如果 ISR 有活副本且 Controller 稳定,多半是 Controller 没有成功下发指令,继续查网络连通性和日志。
- 如果 ISR 为空且业务允许接受短暂不可用,就保持等待副本恢复;只有业务明确允许丢数据时,才在人工确认后临时开启脏选举。
排障最忌讳的就是没看清 ISR 就开脏选举。我见过不少同学一看到分区没 Leader,第一反应就是先开unclean.leader.election.enable=true恢复写入,随后又花一整晚去追回“无缘无故消失”的消息。数据系统里,丢数据往往是不可逆的,等发现已经晚了。
最后说几句个人体会
踩过这次坑之后,我对 Kafka 选举最大的感悟是:它并不神秘,而是一套由外部共识组件(ZooKeeper 或 Raft)保证元数据一致性的确定性流程。Controller 选举决定由谁来当裁判,分区选举决定每个分区的读写路径归于何人,两者层层递进、必须分层排查。真正让人翻车的地方,从来不是“谁当选”这个结果,而是我们在配置里给“选一个坏 Leader”留下了口子,或者在本该等待副本恢复时急着用脏选举换来那几分钟的“可用”。维护 Kafka 久了,你会越来越认同一个原则:先保证元数据稳定,再谈数据可用;先保证数据完整,再追求毫秒级恢复速度。