Controller 与 KRaft:从 ZooKeeper 到内置共识
上一篇看到了副本机制的细节。这一篇解决元数据层的核心问题:谁来决定哪个 replica 是 leader,以及这个决定怎么达成共识。
这个问题容易和副本选举混淆。副本选举本身只是一个结果——“partition-0 的 leader 从 broker-1 变成 broker-2”。真正的问题是:谁有权做出这个决定,这个决定如何传播到集群中的每个 broker,以及当做出决定的那个节点自己挂了会发生什么。
本文只抓一个问题:Kafka 的元数据管理从外部依赖(ZooKeeper)演化到内置共识(KRaft)的完整路径。
ZooKeeper 模式下的 Controller
在 ZooKeeper 模式下,Kafka 集群中有且仅有一个 broker 担任 controller 角色。Controller 是通过 ZooKeeper 的临时节点(ephemeral node)竞选产生的:
1 | |
所有 broker 在启动时尝试在 ZooKeeper 的 /controller 路径创建临时节点。第一个成功创建的 broker 成为 controller,其他 broker 在该节点上设置 watch,等待 controller 失效后重新竞选。
每次 controller 变更时,controller epoch 单调递增。Broker 收到 controller 的请求时会检查 epoch:如果请求中的 epoch 低于本地记录的最新 epoch,说明这是来自旧 controller 的过期请求,直接拒绝。这个机制防止网络分区或延迟导致的"双 controller"问题。
Controller 的职责
Controller 承担了集群中所有元数据变更的决策权:
- Partition leader 选举:当某个 partition 的 leader 所在 broker 宕机时,controller 从该 partition 的 ISR 中选择新 leader,并向相关 broker 发送 LeaderAndIsr 请求。
- ISR 变更处理:Leader 检测到 follower 滞后或恢复后,向 controller 报告 ISR 变更,controller 更新元数据并传播。
- Topic 创建和删除:Controller 监听 ZooKeeper 上的 topic 变更事件,执行 partition 分配和 replica 放置。
- Broker 注册与下线:Controller 监听 /brokers/ids 下的临时节点变化,broker 下线时触发受影响 partition 的 leader 重新选举。
Controller 通过向每个 broker 发送三种请求来传播元数据变更:
| 请求类型 | 用途 |
|---|---|
| LeaderAndIsr | 通知 broker 某个 partition 的 leader/ISR 变化 |
| UpdateMetadata | 通知 broker 更新本地元数据缓存 |
| StopReplica | 通知 broker 停止某个 partition 的副本并可选删除数据 |
ZooKeeper 的角色与瓶颈
ZooKeeper 在 Kafka 集群中扮演三个角色:
第一,会话管理与存活检测。每个 broker 与 ZooKeeper 保持一个会话(session),通过心跳维持。会话超时意味着 broker 被认为已宕机,其注册的临时节点自动删除。
第二,临时节点用于 broker 存活注册。每个 broker 在 /brokers/ids/{brokerId} 创建临时节点,节点内容包含 broker 的地址和端口。Controller 通过 watch 这些节点的变化来感知 broker 上下线。
第三,持久节点用于存储 topic 元数据。Topic 的 partition 分配、replica 列表、配置参数等存储在 /brokers/topics/ 下的持久节点中。
ZooKeeper 成为瓶颈的原因有三个层面:
元数据写放大。Controller 是 Kafka 集群中唯一直接写 ZooKeeper 的组件,但每次元数据变更都涉及多次 ZK 写操作。例如一次 ISR 变更需要:写 ZK 更新 ISR → 读回确认 → 向所有 broker 发送 LeaderAndIsr → 等待响应。当集群有数万个 partition 时,大规模 broker 故障可能触发数千个 partition 的 leader 选举,ZooKeeper 成为串行瓶颈。
脑裂风险。ZooKeeper 的会话超时机制依赖网络连通性。在网络分区场景下,controller 可能与 ZooKeeper 断开连接但仍在运行,此时新 controller 已经选出。虽然 controller epoch 可以防止旧 controller 的请求被接受,但在过渡期间元数据传播可能出现不一致窗口。
部署复杂性。运维一个 Kafka 集群需要同时运维一个 ZooKeeper 集群。两套系统有各自的配置、监控、升级和容量规划,运维成本是双倍的。
KRaft:内置的 Raft 共识
KIP-500 提出了 KRaft(Kafka Raft)模式,目标是用 Kafka 内置的 Raft 共识协议替代 ZooKeeper 进行元数据管理。自 Kafka 3.3 起 KRaft 被标记为生产就绪,ZooKeeper 模式从 Kafka 3.5 起被标记为 deprecated。
KRaft 的核心思路:把元数据本身当作一个 Kafka topic 来管理。
1 | |
KRaft 架构的三种角色
KRaft 集群中的节点有三种角色:
Active Controller:controller quorum 中通过 Raft 选举产生的 leader。只有 active controller 能写入 __cluster_metadata topic。所有元数据变更请求(创建 topic、ISR 变更等)都发送到 active controller。
Voter:参与 Raft 选举投票的节点。Voter 维护 __cluster_metadata 日志的完整副本,可以在 active controller 故障时成为新的 active controller。典型配置是 3 个或 5 个 voter。
Observer:不参与投票但被动跟踪元数据日志的节点。普通 broker 在 KRaft 模式下就是 observer——它们从 active controller 拉取 __cluster_metadata 的增量更新来同步元数据。Observer 不需要维护完整的元数据日志历史,可以通过 snapshot 快速追上。
在小型集群中,controller 角色和 broker 角色可以运行在同一个 JVM 进程中(combined 模式)。在大型集群中,controller 建议独立部署(isolated 模式),避免数据面流量干扰元数据共识。
__cluster_metadata:元数据即日志
ZooKeeper 模式下的元数据存储在 ZK 的树状 znode 结构中,每个 znode 是一个独立的键值对。KRaft 模式下的元数据存储在 __cluster_metadata 这个内部 topic 的日志中。
这个转变的意义:
- 元数据变更有天然的顺序。每条元数据变更都有一个 offset,变更的因果关系通过 offset 顺序体现。
- 增量同步取代全量同步。Broker 只需要从自己已知的 offset 开始拉取增量,不需要像 ZK 模式那样在 controller failover 后做全量元数据推送。
- Snapshot 替代 ZK 的持久节点。当日志过长时,controller 生成 snapshot 压缩历史记录,新节点或长时间离线的 broker 可以从 snapshot 快速恢复。
从 ZooKeeper 迁移到 KRaft
Kafka 提供了三阶段迁移路径:
1 | |
第一阶段:部署 KRaft controller quorum,与现有 ZK 模式的 broker 共存。KRaft controller 开始记录元数据日志,同时 ZK 继续提供服务。
第二阶段:将 broker 逐个切换到 KRaft 模式。Broker 的 metadata 来源从 ZK 切换到 KRaft controller。此阶段 ZK 和 KRaft 双写,保证回滚能力。
第三阶段:确认所有 broker 都已切换到 KRaft 模式后,执行 kafka-metadata.sh 的 finalize 命令,断开 ZK 连接,进入纯 KRaft 模式。此后 ZK 集群可以下线。
迁移过程中需要关注的版本约束:broker 和 controller 的 inter.broker.protocol.version 和 metadata.version 必须兼容。
实验:观察 KRaft 模式下的元数据传播
在 KRaft 模式的集群中,可以通过 kafka-metadata.sh 工具观察元数据状态:
1 | |
输出包含当前 active controller、voter 列表、observer 列表以及各节点的元数据 offset。
创建一个 topic 后,可以观察到 __cluster_metadata 的 offset 增长:
1 | |
每个 topic 创建操作会在 __cluster_metadata 中写入多条记录:TopicRecord(topic 本身)和多条 PartitionRecord(每个 partition 的 replica 分配和 leader 选举结果)。
与 ZK 模式下的元数据传播对比:ZK 模式中 controller 需要向每个 broker 逐一发送 LeaderAndIsr 和 UpdateMetadata 请求;KRaft 模式中 broker 作为 observer 主动拉取 __cluster_metadata 日志的增量。拉取模式在大规模集群中的扩展性更好——controller 不需要维护到每个 broker 的推送连接,broker 可以按自己的速度追赶。
模式提炼
Kafka 从 ZooKeeper 到 KRaft 的演化体现了一个更普遍的架构模式:内置共识 vs 外部协调服务。
外部协调服务模式:系统本身不具备共识能力,依赖一个独立的协调服务(ZooKeeper、etcd、Consul)来管理元数据和选举。优点是关注点分离,系统只需要实现业务逻辑;缺点是引入了额外的运维对象和故障域,两套系统的版本兼容、网络连通性、性能特征都需要独立管理。
内置共识模式:系统自己实现共识协议来管理元数据。优点是简化部署和运维,消除外部依赖的故障域;缺点是增加了系统自身的复杂性,共识协议的正确性需要系统自己保证。
这个模式在分布式系统中反复出现。Kubernetes 从外挂 etcd 开始,至今仍然依赖 etcd(但社区有去 etcd 的讨论)。MongoDB 的 Replica Set 使用内置的 Raft 变体。MySQL Group Replication 使用内置的 Paxos 变体。TiDB 使用内置的 Multi-Raft。
选择哪种模式取决于系统的成熟度和规模。早期项目倾向于使用外部协调服务来降低自身复杂性;成熟项目在规模增大后倾向于内置共识来简化运维和消除性能瓶颈。Kafka 的演化路径是这个规律的一个典型案例。
工程迁移表
| 概念 | Kafka Controller (ZK) | Kafka KRaft | etcd (K8s) | Consul | RocketMQ NameServer |
|---|---|---|---|---|---|
| 共识协议 | ZAB (ZK 内部) | Raft (内置) | Raft | Raft + Gossip | 无共识(AP 设计) |
| 元数据存储 | ZK znode 树 | __cluster_metadata 日志 | key-value store | key-value + service catalog | 内存路由表 |
| leader 选举 | ZK 临时节点竞争 | Raft 选举 | Raft 选举 | Raft 选举 | 无 leader(每个实例独立) |
| 元数据同步 | Controller 推送 | Observer 拉取日志 | Watch 推送 | Gossip 协议 | Broker 定期上报,Client 定期拉取 |
| 部署依赖 | 需要独立 ZK 集群 | 无外部依赖 | 独立 etcd 集群 | 独立 Consul 集群 | 独立 NameServer 进程 |
| 可扩展性瓶颈 | ZK 写入吞吐 | Controller quorum 大小 | etcd 存储大小 | Gossip 收敛时间 | 无强一致性约束 |
常见误解
KRaft 就是 Raft。KRaft 基于 Raft 协议但做了针对 Kafka 场景的适配。标准 Raft 论文中 log entry 的含义是状态机命令,KRaft 中 log entry 是元数据变更记录。KRaft 还引入了 snapshot 机制来压缩历史日志,以及 observer 角色来支持大规模 broker 集群的元数据同步。这些都不是标准 Raft 的内容。
ZooKeeper 存储消息。ZooKeeper 只存储 Kafka 的元数据(topic 配置、partition 分配、consumer group offset 等),不存储任何消息数据。消息数据存储在各 broker 的本地日志文件中。一个 3 节点 ZK 集群的数据量通常只有几十 MB,而一个 Kafka 集群的消息数据可能有几十 TB。
KRaft 替代了 ZooKeeper 的所有功能。KRaft 替代的是 Kafka 对 ZooKeeper 的依赖,不是 ZooKeeper 本身的能力。如果集群中有其他系统(如 HBase、Hadoop)也依赖同一个 ZooKeeper 集群,迁移到 KRaft 后 ZooKeeper 仍然需要为那些系统服务。KRaft 解决的是 Kafka 的部署简化和性能优化,不是取消 ZooKeeper 这个项目。
练习
- 用 Docker Compose 分别搭建一个 ZK 模式和一个 KRaft 模式的 3 broker 集群。对比两种模式下创建 100 个 topic(每个 10 partition)的耗时。
- 在 KRaft 集群中,用 kafka-metadata.sh 工具查看 __cluster_metadata 的当前 offset、snapshot 位置和 quorum 成员状态。
- 停止 KRaft 集群的 active controller,观察新 controller 选举的耗时和 broker 侧元数据的更新延迟。
- 阅读 KIP-500 的 Motivation 章节,总结 ZooKeeper 模式在大规模集群(100+ broker、100K+ partition)下遇到的具体性能问题。
系列导航
| 序号 | 主题 |
|---|---|
| 00 | 导读:为什么 Kafka 的核心是一根日志 |
| 01 | 架构总览:Broker、Controller 与元数据管理 |
| 02 | 日志存储:Segment、Index 与零拷贝 |
| 03 | Producer 内部机制:攒批、分区与 acks |
| 04 | 幂等 Producer 与序列号:消息不重不丢的第一层 |
| 05 | Consumer Group 协议:分配、重平衡与静态成员 |
| 06 | Offset 管理:提交、重置与消费语义 |
| 07 | 副本与 ISR:高可用的代价和折中 |
| 08 | Controller 与 KRaft:从 ZooKeeper 到内置共识 |
| 09 | Exactly-Once 与事务:跨 partition 的原子写入 |
| 10 | 日志压缩:把 topic 当 KV 表用 |
| 11 | Kafka Streams:在日志之上构建流处理 |
| 12 | Kafka Connect:标准化的数据管道 |
| 13 | Schema Registry 与数据治理:给消息加上契约 |
| 14 | 性能模型:吞吐、延迟与调优思路 |
| 15 | 安全体系:认证、授权与加密 |
| 16 | 生产运维:集群扩缩、监控指标与故障排查 |
| 17 | Kafka 与 RocketMQ:两种消息系统的设计选择 |
参考资料
- KIP-500 - Replace ZooKeeper with a Self-Managed Metadata Quorum:https://cwiki.apache.org/confluence/display/KAFKA/KIP-500%3A+Replace+ZooKeeper+with+a+Self-Managed+Metadata+Quorum
- Apache Kafka 官方文档 - KRaft:https://kafka.apache.org/documentation/#kraft
- Colin McCabe. KIP-500: Apache Kafka Without ZooKeeper. Confluent Blog:https://www.confluent.io/blog/removing-zookeeper-dependency-in-kafka/
- Apache Kafka 官方文档 - ZooKeeper to KRaft Migration:https://kafka.apache.org/documentation/#kraft_zk_migration
- Diego Ongaro, John Ousterhout. In Search of an Understandable Consensus Algorithm (Raft). USENIX ATC 2014.
