上一篇看到了副本机制的细节。这一篇解决元数据层的核心问题:谁来决定哪个 replica 是 leader,以及这个决定怎么达成共识。

这个问题容易和副本选举混淆。副本选举本身只是一个结果——“partition-0 的 leader 从 broker-1 变成 broker-2”。真正的问题是:谁有权做出这个决定,这个决定如何传播到集群中的每个 broker,以及当做出决定的那个节点自己挂了会发生什么。

本文只抓一个问题:Kafka 的元数据管理从外部依赖(ZooKeeper)演化到内置共识(KRaft)的完整路径。

ZooKeeper 模式下的 Controller

在 ZooKeeper 模式下,Kafka 集群中有且仅有一个 broker 担任 controller 角色。Controller 是通过 ZooKeeper 的临时节点(ephemeral node)竞选产生的:

1
2
3
4
5
6
7
8
9
10
11
12
              ZooKeeper Ensemble
┌───────────────────┐
│ /controller │ ← 临时节点
│ /brokers/ids/ │ ← broker 注册
│ /brokers/topics/ │ ← topic 元数据
│ /admin/ │ ← 管理操作
└───────┬───────────┘

┌──────────────┼──────────────┐
│ │ │
Broker-0 Broker-1 Broker-2
(Controller)

所有 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
KRaft 模式
┌────────────────────────────────────────┐
│ Controller Quorum │
│ ┌──────────┐ ┌────────┐ ┌────────┐ │
│ │ Active │ │ Voter │ │ Voter │ │
│ │Controller│ │ │ │ │ │
│ └────┬─────┘ └────────┘ └────────┘ │
│ │ │
│ ▼ │
│ __cluster_metadata (内部 topic) │
│ [epoch=1,offset=0] leader=B0,P0 │
│ [epoch=1,offset=1] isr={0,1,2},P0 │
│ [epoch=1,offset=2] config:retention │
... │
└────────────────────┬───────────────────┘
│ 元数据日志推送
┌───────────┼───────────┐
▼ ▼ ▼
Broker-0 Broker-1 Broker-2

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
2
3
4
5
6
ZooKeeper 模式              KRaft 迁移模式              纯 KRaft 模式
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ ZK 管理元数据 │ ──→ │ KRaft Controller │ ──→ │ 纯 KRaft │
│ Controller │ │ + ZK 双写 │ │ 无 ZK 依赖 │
│ 依赖 ZK 选举 │ │ ZK 提供兼容 │ │ │
└──────────────┘ └──────────────────┘ └──────────────┘

第一阶段:部署 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
2
3
4
5
6
7
# 查看 controller quorum 状态
kafka-metadata.sh --snapshot /var/kafka-logs/__cluster_metadata-0/00000000000000000000.log \
--cluster-id <cluster-id>

# 或者通过 Admin API 查看
kafka-metadata.sh --bootstrap-controller localhost:9093 \
status

输出包含当前 active controller、voter 列表、observer 列表以及各节点的元数据 offset。

创建一个 topic 后,可以观察到 __cluster_metadata 的 offset 增长:

1
2
3
4
5
6
7
8
9
10
# 创建 topic 前记录 offset
kafka-metadata.sh --bootstrap-controller localhost:9093 status

# 创建 topic
kafka-topics.sh --bootstrap-server localhost:9092 \
--create --topic kraft-test \
--partitions 3 --replication-factor 3

# 再次查看 offset
kafka-metadata.sh --bootstrap-controller localhost:9093 status

每个 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 这个项目。

练习

  1. 用 Docker Compose 分别搭建一个 ZK 模式和一个 KRaft 模式的 3 broker 集群。对比两种模式下创建 100 个 topic(每个 10 partition)的耗时。
  2. 在 KRaft 集群中,用 kafka-metadata.sh 工具查看 __cluster_metadata 的当前 offset、snapshot 位置和 quorum 成员状态。
  3. 停止 KRaft 集群的 active controller,观察新 controller 选举的耗时和 broker 侧元数据的更新延迟。
  4. 阅读 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:两种消息系统的设计选择

参考资料