上一篇确立了追加日志是 Kafka 的核心抽象。这一篇展开这根日志运行在什么样的集群架构上。

一个常见的简化说法是"Kafka 是一组 broker"。更准确的描述是:Kafka 集群由若干 broker 进程组成,其中一个 broker 承担 controller 角色,负责元数据管理和 partition leader 选举。

本文只抓一个问题:从 topic 到磁盘目录,数据和元数据分别如何组织。

Broker:存储日志并服务读写请求

Broker 是 Kafka 集群中的工作进程。每个 broker 做两件事:

  1. 存储分配给它的 partition 的日志文件(.log、.index、.timeindex)。
  2. 服务 produce 请求(追加记录到日志尾部)和 fetch 请求(按 offset 从日志中读取记录)。

一个 broker 可以同时持有多个 topic 的多个 partition。每个 partition 在磁盘上是一个独立的目录,目录名格式为 {topic名}-{partition号}

1
2
3
4
5
6
7
8
9
10
11
broker-0 的数据目录 (log.dirs=/data/kafka-logs)
├── orders-0/ ← topic "orders", partition 0
│ ├── 00000000000000000000.log
│ ├── 00000000000000000000.index
│ └── 00000000000000000000.timeindex
├── orders-3/ ← topic "orders", partition 3
│ └── ...
├── users-1/ ← topic "users", partition 1
│ └── ...
└── __consumer_offsets-12/ ← 内部 topic,存储 consumer group 的 offset
└── ...

每个 partition 目录内部的文件组织将在第 02 篇展开。这里只需要记住一个映射关系:

1
2
3
4
Topic (逻辑概念)
└── Partition (逻辑分片,编号 0 ~ N-1)
└── Log (物理目录,一组 segment 文件)
└── Segment (.log + .index + .timeindex)

Topic、Partition 与 Log 的映射

一个 topic 是一个逻辑名称。创建 topic 时指定 partition 数量,每个 partition 是一根独立的追加日志。Partition 之间没有全局 offset 关系——partition 0 的 offset 5 和 partition 1 的 offset 5 是完全无关的两条记录。

Partition 是 Kafka 并行度的基本单位:

  • produce 端:不同 partition 可以由不同 broker 并行接收写入。
  • consume 端:同一个 consumer group 内,每个 partition 只分配给一个 consumer 实例。

这意味着一个 topic 的 partition 数量决定了该 topic 的消费并行度上限。

1
2
3
4
5
6
7
8
Topic: orders (6 partitions, replication factor = 2)

Partition 0 ──→ Broker 0 (leader), Broker 1 (follower)
Partition 1 ──→ Broker 1 (leader), Broker 2 (follower)
Partition 2 ──→ Broker 2 (leader), Broker 0 (follower)
Partition 3 ──→ Broker 0 (leader), Broker 2 (follower)
Partition 4 ──→ Broker 1 (leader), Broker 0 (follower)
Partition 5 ──→ Broker 2 (leader), Broker 1 (follower)

每个 partition 有一个 leader 副本和零到多个 follower 副本。Produce 和 fetch 请求只发往 leader 所在的 broker(截至 Kafka 2.4+,follower 也可以服务 fetch 请求,但需要显式配置 replica.selector.class)。

Controller:集群的元数据中枢

Kafka 集群中有且只有一个 broker 在任意时刻担任 controller 角色。Controller 不是一个独立的进程——它是某个 broker 进程内的一个角色模块。

Controller 的职责包括:

  • partition leader 选举:当某个 partition 的 leader 所在 broker 下线时,controller 从 ISR(In-Sync Replicas)列表中选出新 leader。
  • partition 分配:当创建新 topic 或扩展 partition 时,controller 决定每个 partition 的副本放在哪些 broker 上。
  • 元数据传播:controller 将 partition 的 leader 分布、ISR 列表等元数据推送给集群中的所有 broker。
1
2
3
4
5
6
7
8
9
10
11
12
13
元数据流向:

Client Broker (any) Controller
│ │ │
│── Metadata Request ──→ │ │
│ │ (本地缓存的元数据副本) │
│←── Metadata Response ── │ │
│ (包含每个 partition │ │
│ 的 leader broker) │ │
│ │ │
│ │←── UpdateMetadata ──────── │
│ │ (controller 推送最新 │
│ │ partition 分布) │

客户端发起 Metadata Request 时,可以发给任意一个 broker。每个 broker 本地维护一份元数据缓存,这份缓存由 controller 通过 UpdateMetadata 请求保持同步。客户端拿到 metadata response 之后,就知道每个 partition 的 leader 在哪个 broker 上,后续的 produce/fetch 请求直接发给对应的 leader broker。

ZooKeeper 模式与 KRaft 模式

Controller 的选举和元数据存储有两种模式,取决于 Kafka 的部署方式。

ZooKeeper 模式(Kafka 3.3 之前的默认模式):

  • Controller 选举通过 ZooKeeper 的临时节点(ephemeral node)实现:所有 broker 竞争创建 /controller 节点,创建成功的 broker 成为 controller。
  • 元数据存储在 ZooKeeper 的 znode 路径中:/brokers/ids/ 存储 broker 注册信息,/brokers/topics/ 存储 topic 配置和 partition 分配。
  • Broker 下线时,ZooKeeper 通过 session 过期检测到变化,通知 controller 触发 leader 重新选举。

KRaft 模式(Kafka 3.3+ 引入,3.5+ 生产就绪,自 Kafka 4.0 起 ZooKeeper 模式不再支持):

  • 不依赖外部 ZooKeeper 集群。元数据管理由一组专门的 controller 节点通过 Raft 共识协议完成。
  • 元数据存储在内部 topic __cluster_metadata 中,这个 topic 本身就是一根追加日志——元数据的变更记录以日志的形式持久化,与 Kafka 数据存储的核心抽象一致。
  • Controller 节点可以与 broker 共进程(combined 模式),也可以独立部署(isolated 模式)。生产环境推荐 isolated 模式,让 controller 的资源消耗不影响数据面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ZooKeeper 模式                          KRaft 模式

┌──────────┐ ┌──────────────────────┐
│ ZooKeeper │ ← session/watch │ Controller Quorum │
│ Ensemble │ │ (3 或 5 个节点, │
└────┬─────┘ │ 内部 Raft 协议) │
│ └──────────┬───────────┘
│ 元数据读写 │ 元数据通过
│ │ __cluster_metadata
▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Broker 0 │ │ Broker 1│ │ Broker 0 │ │ Broker 1│
│(controller)│ │ │ │ │ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘

两种模式下 broker 服务 produce/fetch 请求的方式完全相同。区别仅在于元数据的存储和 controller 的选举机制。

实验:启动 3-broker 集群并观察日志目录

以下实验使用 KRaft 模式启动一个 3-broker 集群,创建一个 6 partition 的 topic,然后观察日志目录的分布。

准备三份配置文件,关键差异参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# broker-0: config/kraft/server-0.properties
node.id=0
controller.quorum.voters=0@localhost:9093,1@localhost:9094,2@localhost:9095
listeners=PLAINTEXT://localhost:9092,CONTROLLER://localhost:9093
log.dirs=/tmp/kafka-logs-0
process.roles=broker,controller

# broker-1: config/kraft/server-1.properties
node.id=1
controller.quorum.voters=0@localhost:9093,1@localhost:9094,2@localhost:9095
listeners=PLAINTEXT://localhost:9192,CONTROLLER://localhost:9094
log.dirs=/tmp/kafka-logs-1
process.roles=broker,controller

# broker-2: config/kraft/server-2.properties
node.id=2
controller.quorum.voters=0@localhost:9093,1@localhost:9094,2@localhost:9095
listeners=PLAINTEXT://localhost:9292,CONTROLLER://localhost:9095
log.dirs=/tmp/kafka-logs-2
process.roles=broker,controller

格式化并启动:

1
2
3
4
5
6
7
8
9
10
KAFKA_CLUSTER_ID="$(bin/kafka-storage.sh random-uuid)"
for i in 0 1 2; do
bin/kafka-storage.sh format -t $KAFKA_CLUSTER_ID \
-c config/kraft/server-$i.properties
done

# 分别在三个终端启动
bin/kafka-server-start.sh config/kraft/server-0.properties
bin/kafka-server-start.sh config/kraft/server-1.properties
bin/kafka-server-start.sh config/kraft/server-2.properties

创建 topic 并观察分布:

1
2
3
4
5
6
bin/kafka-topics.sh --create --topic cluster-demo \
--partitions 6 --replication-factor 2 \
--bootstrap-server localhost:9092

bin/kafka-topics.sh --describe --topic cluster-demo \
--bootstrap-server localhost:9092

输出类似:

1
2
3
4
5
6
7
Topic: cluster-demo  TopicId: ...  PartitionCount: 6  ReplicationFactor: 2
Topic: cluster-demo Partition: 0 Leader: 0 Replicas: 0,1 Isr: 0,1
Topic: cluster-demo Partition: 1 Leader: 1 Replicas: 1,2 Isr: 1,2
Topic: cluster-demo Partition: 2 Leader: 2 Replicas: 2,0 Isr: 2,0
Topic: cluster-demo Partition: 3 Leader: 0 Replicas: 0,2 Isr: 0,2
Topic: cluster-demo Partition: 4 Leader: 1 Replicas: 1,0 Isr: 1,0
Topic: cluster-demo Partition: 5 Leader: 2 Replicas: 2,1 Isr: 2,1

检查每个 broker 的日志目录:

1
2
3
4
5
6
7
8
ls /tmp/kafka-logs-0/ | grep cluster-demo
# cluster-demo-0 cluster-demo-2 cluster-demo-3 cluster-demo-4

ls /tmp/kafka-logs-1/ | grep cluster-demo
# cluster-demo-0 cluster-demo-1 cluster-demo-4 cluster-demo-5

ls /tmp/kafka-logs-2/ | grep cluster-demo
# cluster-demo-1 cluster-demo-2 cluster-demo-3 cluster-demo-5

可以验证:每个 broker 上出现的 partition 目录数量与 --describe 输出中该 broker 出现在 Replicas 列表中的次数一致。Leader 和 follower 的日志目录结构相同——区别仅在于 leader 接受写入,follower 从 leader 拉取数据。

模式提炼

Kafka 的 controller 是一种集中式元数据管理器(centralized metadata controller)模式。这个模式在分布式系统中反复出现:

  • 一个专门的角色负责全局决策(leader 选举、分片分配、元数据一致性)。
  • 数据面节点从元数据管理器获取路由信息,直接点对点传输数据。
  • 元数据管理器的可用性通过冗余(多节点选举或 quorum)保证。

这个模式把"全局协调的一致性需求"集中在一个小集群上,让数据面节点可以无状态地横向扩展。代价是元数据管理器成为潜在的单点——需要额外的选举和容错机制来保护。

工程迁移表

维度 Kafka Controller RocketMQ NameServer HDFS NameNode
角色 partition leader 选举、元数据管理 路由信息管理(不做选举) block 映射、namespace 管理
选举机制 ZK 临时节点 / KRaft Raft 选举 无选举,NameServer 对等部署 HA:ZK 选举或 Observer 模式
元数据存储 ZK znode / __cluster_metadata topic 内存(broker 定期上报) fsimage + editlog
元数据一致性 强一致(ZK/Raft) 最终一致(各 NameServer 独立接收心跳) 强一致(HA editlog 同步)
数据面路由 client 缓存 metadata,直连 leader broker client 缓存路由表,直连 broker client 从 NameNode 获取 block 位置,直连 DataNode
单点风险 是(通过选举恢复) 否(对等部署) 是(通过 HA 恢复)

常见误解

误解一:“Controller 是一个独立的服务。”

在 ZooKeeper 模式下,controller 是某个 broker 进程内的角色,与普通 broker 共享同一个 JVM。在 KRaft 模式的 combined 部署下同理。只有 KRaft 的 isolated 模式才会将 controller 部署为独立进程,但即便如此,controller 进程运行的也是 Kafka broker 的代码,只是 process.roles 配置不同。

误解二:“ZooKeeper 存储消息。”

ZooKeeper 只存储集群元数据(broker 注册信息、topic 配置、partition 分配、consumer group offset 的旧版存储)。消息数据存储在 broker 本地磁盘的日志文件中,不经过 ZooKeeper。ZooKeeper 的数据量通常在 MB 级别,而 Kafka 的消息存储可以达到 TB 级别。

误解三:“所有请求都经过 controller。”

Produce 和 fetch 请求直接发送给 partition 的 leader broker,不经过 controller。Controller 只处理管理面操作:leader 选举、partition 重分配、topic 创建/删除。数据面的吞吐不受 controller 性能制约。

练习

  1. 在上面的 3-broker 实验中,kill 掉其中一个 broker 进程,然后再次执行 kafka-topics.sh --describe。观察哪些 partition 的 leader 发生了变化,ISR 列表有何变化。重启该 broker 后再次观察 ISR 的恢复过程。

  2. 使用 kafka-metadata.sh 工具(KRaft 模式下可用)查看 __cluster_metadata topic 的内容,找到 topic 创建和 partition 分配的记录。思考这些记录与 ZooKeeper znode 的对应关系。

  3. 对比 RocketMQ 的 NameServer 设计:RocketMQ 选择让所有 NameServer 对等、无选举、最终一致。Kafka 选择单 controller 加强一致性选举。列出两种方案各自的优势和代价。

系列导航

序号 主题
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:两种消息系统的设计选择

参考资料

  1. Apache Kafka Documentation — Design: Replication. https://kafka.apache.org/documentation/#replication
  2. 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
  3. Apache Kafka Documentation — KRaft Configuration. https://kafka.apache.org/documentation/#kraft
  4. Neha Narkhede, Gwen Shapira, Todd Palino. Kafka: The Definitive Guide. O’Reilly. Chapter 5: Kafka Internals.
  5. Apache Kafka Source Code — kafka.controller.KafkaController. https://github.com/apache/kafka