前置回顾
- 第 09 篇的动手实验里,停掉 Leader Broker 后新 Leader 自动上位——那个"自动"背后有一个决策者,本篇揭开它
- 第 02 篇提过 Producer 通过 metadata 知道谁负责哪个 Partition,本篇讲这份 metadata 从哪来、怎么传播
- 第 04 篇的 Group Coordinator 与本篇的 Controller 是两个独立角色,末尾 QA 对比
一句话总结
Controller 是集群的"大脑":某个 Broker 兼任的特殊角色,同一时刻只有一个,负责选举 Partition Leader、管理 ISR、处理 Topic 增删改,并把最新的集群地图广播出去.
地图(元数据)存在哪,决定了架构分成两代:ZooKeeper 模式(旧)与 KRaft 模式(新).理解 Controller,就理解了集群怎么自治.
从一次故障复盘开始
第 09 篇的实验:停掉 kafka-0,几秒后 Partition 0 的 Leader 从 0 自动换成 1.把这"几秒"拆开,背后是一连串动作:
|
这四步必须由同一个角色来做.如果各 Broker 自行其是,可能两个 Broker 同时认为自己是 P0 的 Leader——脑裂:两边都收写入,数据分叉,这是分布式系统最怕的结局.
集群需要一个单一决策者:Controller.
Controller 是什么
一句话定义:Controller 是集群中某个 Broker 兼任的特殊角色,同一时刻集群里只有一个 active controller.它不存业务数据,只做两件事:决策与通知.
职责全景:
| 职责 | 说明 |
|---|---|
| Broker 上下线检测 | 维护存活 Broker 列表,发现宕机 |
| Partition Leader 选举 | 按第 09 篇的规则从 ISR 选新 Leader |
| ISR 管理 | ISR 伸缩最终由它落定并广播 |
| Topic 管理 | 创建/删除 Topic、增加分区 |
| 分区重分配 | 分区在 Broker 间迁移的调度者 |
| 元数据传播 | 把最新集群地图推给所有 Broker |
Controller = 值班经理:每个员工照常干活,值班经理额外负责排班、应对突发、把变更通知到所有人.
值班经理也是员工(Broker)之一,只是多了一副职责.他请假(宕机)了,换一个人顶上,业务照转.
注意角色叠加:一台 Broker 可能同时是全局 Controller、P0 的 Leader、P1 的 Follower.三个维度互不冲突,只是同一进程承担了多份职责.
元数据:Controller 的依据和产物
Controller 决策的依据、决策后的产物,都是元数据——集群状态的全局地图:
|
它有两个消费方:
| 消费方 | 获取方式 | 更新时机 |
|---|---|---|
| Broker | Controller 主动推送 | 元数据变更时 |
| 客户端 | 连任意 Broker 拉取(MetadataRequest) | 定期刷新 + 出错立即刷新 |
客户端的行为在第 02 篇见过:Producer 发消息前先通过 metadata 知道目标 Partition 的 Leader 在哪.这套机制的细节:
- 定期刷新:
metadata.max.age.ms,默认 5 分钟 - 错误触发:请求打到旧 Leader 收到 NotLeader 错误,立即刷新 metadata 并重试,这是主力路径
- 不依赖 Controller 存活:客户端连的是任意 Broker,不是 Controller
老架构:ZooKeeper 模式
ZooKeeper(下文简称 ZK)是独立的分布式协调服务,提供树形数据存储、临时节点(会话断开自动消失)、watch(变更通知).Kafka 用了几十年,直到被 KRaft 取代.
ZK 里存了什么
|
Controller 选举:抢临时节点
所有 Broker 抢着创建 /controller 临时节点,谁先创建成功谁当 Controller.临时节点绑定创建者的会话:Controller 宕机,会话结束,节点自动消失,其余 Broker 通过 watch 察觉到,再次开抢.
为什么被淘汰
| 痛点 | 说明 |
|---|---|
| 元数据双写 | 状态同时存在 ZK 树和 Broker 本地日志,两处要对齐,历史上 bug 集中地 |
| 切换慢 | 新 Controller 上位要先从 ZK 拉全量元数据重建视图,分区越多越慢 |
| 多一套系统 | ZK 集群要单独部署、监控、调优,还要和 Kafka 版本匹配 |
| 规模瓶颈 | ZK 的节点数和 watch 数撑不起百万分区 |
新架构:KRaft 模式
KRaft(Kafka Raft)把元数据搬回 Kafka 自己手里.时间线:
| 版本 | 事件 |
|---|---|
| 3.3 | KRaft 生产可用 |
| 3.5 | ZooKeeper 模式标记弃用 |
| 4.0 | ZooKeeper 模式移除 |
新集群一律 KRaft,没有例外.
核心变化
元数据不再放 ZK,而是存进 Kafka 自己的内部日志 __cluster_metadata,用 Raft 共识协议复制到专门的 controller 节点上:
|
Controller 选举:Raft 协议
quorum 内部走 Raft 选举,选出 metadata leader,它同时就是 active controller.因为元数据日志已经复制到 quorum 所有成员,新 Leader 无需从外部加载任何状态,秒级接任——这正是 ZK 模式"切换慢"的解药.
两代架构对比
| 维度 | ZooKeeper 模式 | KRaft 模式 |
|---|---|---|
| 外部依赖 | ZK 集群 | 无 |
| 元数据存储 | ZK 树 + Broker 本地(双写) | __cluster_metadata 日志(单源) |
| Controller 选举 | 抢临时节点 | Raft 选举 |
| 切换速度 | 重新加载全量元数据 | 日志已复制,秒级 |
| 分区规模 | 数十万级 | 百万级 |
回看第 07 篇 compose 的参数,现在都能对上号:
|
合起来:Leader 换人的完整流程
第 09 篇讲了选举规则(只从 ISR 选、截断到 HW),本篇补上"谁来执行".KRaft 模式下完整链路:
|
客户端这条链路值得单独强调:5 分钟定期刷新只是兜底,真正让故障在秒级恢复的是错误触发.对幂等 Producer,这次"失败 + 重试"不会产生重复消息(第 03 篇讲过 PID + Seq 去重),所以链路里没有消息丢失也没有重复.
动手验证
环境沿用第 07 篇的 compose(3 个 combined 节点).找当前 Controller:
|
|
CurrentVoters=[0,1,2]:quorum 的三个成员LeaderId=1:kafka-1 在当 active controllerHighWatermark:元数据日志的长度,每次 Topic 增删、Leader 变更都会增长
验证切换:
|
生产注意事项
新集群直接 KRaft
4.0 起 ZK 模式已被移除,新集群没有选择成本.老集群的 ZK 迁移有官方工具(kraft 迁移命令),规划好窗口逐步执行.
quorum 用奇数节点
Raft 靠多数派共识:3 节点容忍 1 个故障,5 节点容忍 2 个.偶数没有收益还降低可用性.
规模再大也不建议超过 5 个 controller,quorum 的写延迟会随成员数上升.
元数据变更操作节制
Topic 创建/删除、分区重分配这类操作全部打到 Controller.批量脚本创建上千个 Topic 等于对 Controller 发起风暴,会拖慢整个集群的元数据响应.
需要大量建 Topic 时,分批、限速.
combined 模式的隐患
combined 节点身兼 broker 与 controller 两角:业务流量高峰会挤压 Controller 的 CPU 与网络,元数据操作变慢;Controller 故障时它上面的业务分区也要跟着迁移.
大规模集群建议 controller 分离部署,独立小规格机器即可,不存业务数据.
监控 ActiveControllerCount
第 07 篇列过:ActiveControllerCount 必须恒等于 1.出现 0(选举中)或 2(脑裂,极少见)都要立即告警.
Controller 挂了集群还能用吗?
读写不受影响:各分区的 Leader 继续服务,客户端已有 metadata 照常工作.
受影响的是管理操作:期间无法创建 Topic、无法切换 Leader.直到新 Controller 选出(秒级),这段时间如果恰好还有 Broker 宕机,它的 Leader 切换会被推迟——但不会丢数据.
Controller 和 Group Coordinator 是什么关系?
两个完全独立的角色,可能落在同一台 Broker 上,职责无关:
- Controller:管集群级状态(Broker、Topic、Partition),本篇主角
- Group Coordinator:管消费者组(成员关系、offset 提交),第 04 篇主角
客户端 5 分钟才刷新一次 metadata,期间会不会一直写错 Leader?
不会.5 分钟刷新只是兜底,主力是错误触发:写旧 Leader 收到 NotLeader,客户端立即拉新 metadata 重试,通常几百毫秒内恢复.
所以生产代码里别把 NotLeader 这类错误静默吞掉,它正是恢复机制的一部分.
combined 模式下 controller 节点挂了,元数据会丢吗?
不会.元数据日志通过 Raft 复制到 quorum 全部成员,任一成员都有完整副本;业务数据由第 09 篇的副本机制保护.
两个维度各丢各的,互不连累.
为什么元数据日志叫 `__cluster_metadata`,它和我们业务 Topic 有什么区别?
机制上几乎没区别:单分区日志 + Raft 复制 + compact 清理,和 __consumer_offsets 同一套路.
区别在于它不对外,客户端读不到,只由 Controller 写——这就是"Log 即数据库"在集群治理上的第一次应用,阶段三第 15 篇会展开.
快速回顾
- Controller:集群唯一决策者,兼任于某个 Broker,负责选举 Leader、管理 ISR、Topic 增删改、广播元数据
- 元数据:broker/topic/partition → leader 的全局地图,Broker 靠推送、客户端靠拉取 + 错误触发刷新
- ZooKeeper 模式:元数据存外部 ZK,抢临时节点当 Controller,双写与切换慢是硬伤,4.0 已移除
- KRaft 模式:元数据进
__cluster_metadata日志,Raft 选举,日志已复制所以切换快,支撑百万分区 - 换主链路:检测 → 盘点 → 选主 → LeaderAndIsr 通知 → 广播,客户端靠错误触发秒级恢复
动手练习
- 找 Controller:用
kafka-metadata-quorum.sh查看 quorum 状态,记录 LeaderId 与 LeaderEpoch. - 观察选举:停掉 Controller 所在 Broker,再查 quorum,对比 LeaderId 与 LeaderEpoch 的变化;期间跑一个消费者,验证消费不断.
- 看元数据增长:创建一个 Topic 再删除,对比操作前后
HighWatermark的增量. - 客户端视角:用
kcat -L -b localhost:9092看客户端拉到的 metadata 全貌(第 07 篇用过),对照本篇的元数据结构. - 复盘一次切换:停 Leader Broker,同时在另一终端持续生产,观察 Producer 经历错误 → 刷新 → 重试后恢复,日志里找到 NotLeader 错误.
- 读 compose 参数:重读第 07 篇 compose 里所有
CONTROLLER开头的参数,逐个说出它们在本篇机制中的角色.
下一篇进入分区再平衡深入:第 04 篇只讲了 rebalance 的概念与两种协议的外在表现,下篇拆开 Eager 与 Cooperative 的实现细节、sticky assignor 的分配算法,以及频繁 rebalance 的排查思路.