Kafka 告别 ZooKeeper:KRaft 模式深度实战——从架构原理到生产级集群部署完全指南
2026 年,Kafka 3.8+ 已全面成熟,KRaft 模式成为事实标准。本文从底层原理出发,手把手带你搞懂 KRaft 是怎么替代 ZooKeeper 的、为什么比 ZK 模式快 10 倍、百万级分区不再是梦,以及如何从零搭建生产级 KRaft 集群并完成老集群迁移。
一、背景:Kafka 为什么非要干掉 ZooKeeper?
1.1 ZooKeeper 模式的原罪
从 2011 年 Kafka 开源第一天起,ZooKeeper 就是它的"外部大脑"。Broker 注册发现、Controller 选举、分区 leader 选举、元数据存储、ACL 管理……几乎所有协调工作都丢给了 ZK。
这个设计在 Kafka 早期完全合理——2011 年的分布式系统几乎默认标配 ZK,HBase、Hadoop、Dubbo 都在用它。但随着 Kafka 规模暴涨,问题开始暴露:
瓶颈一:元数据扩展性天花板
ZK 的元数据模型是树形结构(ZNode),每个分区、每个副本都是树上的节点。当集群分区数超过 10 万时,ZK 开始力不从心。现实中很多大厂的单集群分区数在 50 万~100 万级别,ZK 根本扛不住。
具体表现:
- ZK session 超时频繁触发,Broker 反复重连
- 元数据变更传播延迟飙升,Controller 切换可能需要数十秒
- ZK 集群自身的 Watch 机制在大量分区场景下产生"Watch 风暴"
瓶颈二:运维复杂度翻倍
你部署一套 Kafka,还得单独部署维护一套 ZooKeeper 集群。两套系统,两套监控,两套告警,两套故障排查逻辑。而且 ZK 和 Kafka 的版本耦合还不算紧密,但升级时你需要同时考虑两者的兼容性。
瓶颈三:Controller 单点问题
在 ZK 模式下,Kafka 只有一个 Active Controller(通过 ZK 选举产生)。如果 Controller 所在的 Broker 挂了,ZK 需要重新选举 Controller,这个过程可能导致:
- 分区 leader 选举停滞
- 元数据不可用
- 整个集群短暂"失联"
1.2 KIP-500:Kafka 的"独立宣言"
2019 年,Kafka 社区提出了 KIP-500——用 Kafka 自身的 Raft 协议替代 ZooKeeper。这不是一个简单的"替换",而是 Kafka 架构层面最激进的变革:
- 元数据管理从"外包"变为"自营"
- Controller 从单点变为 Quorum(仲裁组)
- 元数据日志(Metadata Log)成为集群的单一事实来源
- 目标:支持百万级分区、秒级 Controller 故障恢复
经过 Kafka 2.8(预览)、3.3(生产可用)、3.7(移除 ZK 迁移支持、彻底稳定)到 3.8+(全面成熟),KRaft 模式终于在 2026 年成为唯一推荐的生产部署方式。
二、KRaft 架构核心概念
2.1 节点角色:Controller vs Broker
KRaft 模式下,Kafka 节点有两种角色:
┌─────────────────────────────────────────────────────────┐
│ KRaft Cluster │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Controller│ │Controller│ │Controller│ (Quorum) │
│ │ (Node1) │ │ (Node2) │ │ (Node3) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
│ └────────────┼────────────┘ │
│ │ Raft Protocol │
│ ┌────────────┼────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Broker │ │ Broker │ │ Broker │ (Data Plane)│
│ │ (Node1) │ │ (Node2) │ │ (Node3) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────┘
Controller Quorum(控制器仲裁组):
- 由奇数个节点组成(通常 3 个或 5 个)
- 通过 Raft 协议选举出 Active Controller
- 负责元数据管理:分区分配、leader 选举、Broker 上下线
- Controller 节点同时也可以作为 Broker 承担数据存储
Broker(数据节点):
- 负责消息的存储和读写
- 从 Controller 获取元数据快照
- 不参与 Controller 选举
关键区别: 在 ZK 模式下,任何一个 Broker 都可能被选为 Controller(单点)。在 KRaft 模式下,只有配置为 Controller 角色的节点才能参与仲裁。
2.2 元数据日志(Metadata Log):KRaft 的"真相"
KRaft 的核心创新在于 Metadata Log。集群的所有元数据——Topic 列表、分区分配、ISR 状态、ACL 配置——都序列化为一条条记录写入 Metadata Log 中。
// 元数据日志记录结构(简化)
MetadataRecord {
batch: {
records: [
TopicRecord(topicId, name, partitions),
PartitionRecord(topicId, partitionId, leaderEpoch, ...),
// ... 更多记录
]
}
}
这根日志通过 Raft 协议在 Controller Quorum 内复制。任何元数据变更都要经过:
- Propose:Active Controller 接收变更请求
- Append & Replicate:写入本地日志并复制到其他 Controller
- Commit:多数节点确认后,变更生效
- Apply:应用到内存状态机(Metadata Snapshot)
这就是标准的 Raft 流程。与 ZK 模式的区别在于:
- ZK 模式下,元数据散落在 ZK 的 ZNode 树中,Controller 需要监听 + 缓存 + 同步,链路长且脆弱
- KRaft 模式下,Metadata Log 是 单一事实来源(Single Source of Truth),没有中间层
2.3 Raft 协议在 Kafka 中的实现
Kafka 的 Raft 实现(org.apache.kafka.raft)针对元数据场景做了深度优化:
Leader Election:
- heartbeat 超时 → 触发选举
- 获得 majority 投票 → 成为 Active Controller
- 默认选举超时:controller.quorum.election.timeout.ms(默认 1000ms)
Log Replication:
- Leader 追加记录 → 发送给 Follower
- Follower 写入本地日志 → 返回 ACK
- 收到 majority ACK → Commit → 广播 Commit 标记
- 默认超时:controller.quorum.fetch.timeout.ms(默认 2000ms)
与标准 Raft 的差异:
| 特性 | 标准 Raft | Kafka Raft |
|---|---|---|
| 日志内容 | 通用状态机命令 | 仅元数据记录 |
| 快照频率 | 依赖应用层 | 自适应(基于日志大小和内存) |
| Follower 数量 | 全部节点 | 仅 Controller Quorum 内 |
| 性能目标 | 通用一致性 | 低延迟元数据传播 |
三、ZK 模式 vs KRaft 模式:核心差异全解
3.1 架构对比
ZK 模式:
Producer → Broker (Leader) ←→ ZooKeeper ←→ Controller (Broker上)
↑ ↑
Consumer ←───────────────────────────────────────┘
(元数据: ZooKeeper 存储, Controller 缓存, 两条路径)
KRaft 模式:
Producer → Broker (Leader) ←→ Controller Quorum (Raft)
↑ ↑
Consumer ←────────────────┘
(元数据: Metadata Log 单一来源, Broker 从 Controller 拉取)
3.2 性能对比
在社区基准测试中,KRaft 模式在元数据操作上表现出碾压级优势:
| 指标 | ZK 模式 | KRaft 模式 | 提升倍数 |
|---|---|---|---|
| Controller 故障恢复 | 30-60 秒 | 5-15 秒 | 3-5x |
| 元数据传播延迟 | 数秒 | 毫秒级 | 1000x+ |
| 支持分区数上限 | ~10 万 | ~100 万+ | 10x |
| Topic 创建延迟 | 数秒 | <1 秒 | 5-10x |
| Broker 启动时间 | 20-40 秒 | 5-10 秒 | 3-4x |
为什么快这么多?
核心原因有两个:
消除 Watch 延迟:ZK 模式下,Broker 通过 Watch 机制监听元数据变更,变更传播依赖 ZK 的 Watch 通知链路。KRaft 模式下,Broker 直接从 Controller 拉取元数据快照增量,没有中间层。
Controller Quorum 去单点:ZK 模式只有一个 Active Controller,它需要先从 ZK 读取全部元数据再执行决策。KRaft 模式的 Active Controller 本身就是 Quorum 的一部分,元数据已在内存中,决策延迟极低。
3.3 运维对比
| 维度 | ZK 模式 | KRaft 模式 |
|---|---|---|
| 组件数量 | Kafka + ZooKeeper | 仅 Kafka |
| 配置文件 | server.properties + zookeeper.properties | 仅 server.properties |
| 监控指标 | 两套独立监控 | 统一监控体系 |
| 故障排查 | 跨系统排查,复杂 | 统一日志,简单 |
| 滚动升级 | ZK 先升再升 Kafka | 统一滚动升级 |
| 磁盘开销 | ZK 事务日志 + Kafka 数据日志 | 仅 Kafka 数据 + 元数据日志 |
四、生产级 KRaft 集群搭建
4.1 环境准备
# 3 节点集群,每节点同时承担 Controller + Broker 角色
# 节点规划:
# node1: 192.168.1.101, broker.id=1, controller.id=1
# node2: 192.168.1.102, broker.id=2, controller.id=2
# node3: 192.168.1.103, broker.id=3, controller.id=3
# 下载 Kafka 3.8+(2026 年推荐版本)
wget https://archive.apache.org/dist/kafka/3.8.0/kafka_2.13-3.8.0.tgz
tar xzf kafka_2.13-3.8.0.tgz
cd kafka_2.13-3.8.0
4.2 初始化集群 ID
KRaft 模式不再使用 ZK 的 zookeeper.connect,而是用 Cluster ID 来标识集群:
# 生成唯一的 Cluster ID(只执行一次,所有节点共用)
KAFKA_CLUSTER_ID=$(bin/kafka-storage.sh random-uuid)
echo "Cluster ID: $KAFKA_CLUSTER_ID"
4.3 格式化存储目录
每个节点都需要格式化:
# Node 1
bin/kafka-storage.sh format \
--config config/kraft/server1.properties \
--cluster-id $KAFKA_CLUSTER_ID \
--add-scram 'SCRAM-SHA-256=[name=admin,password=secret123]'
# Node 2
bin/kafka-storage.sh format \
--config config/kraft/server2.properties \
--cluster-id $KAFKA_CLUSTER_ID \
--add-scram 'SCRAM-SHA-256=[name=admin,password=secret123]'
# Node 3
bin/kafka-storage.sh format \
--config config/kraft/server3.properties \
--cluster-id $KAFKA_CLUSTER_ID \
--add-scram 'SCRAM-SHA-256=[name=admin,password=secret123]'
注意:
--add-scram是可选的,用于配置 SCRAM 认证。如果不使用认证可以省略。
4.4 核心配置文件详解
Node 1 的配置文件 config/kraft/server1.properties:
# ====== 进程角色 ======
# 同时作为 Controller 和 Broker(推荐小集群)
process.roles=broker,controller
# 如果是大集群,可以分离角色:
# 纯 Controller 节点: process.roles=controller
# 纯 Broker 节点: process.roles=broker
# ====== Controller 标识 ======
node.id=1
# ====== Quorum 配置 ======
# Controller 仲裁组的所有节点投票地址
# 格式: controller.id@host:port
controller.quorum.voters=1@192.168.1.101:9093,2@192.168.1.102:9093,3@192.168.1.103:9093
# Controller 之间的通信监听地址
controller.listener.names=CONTROLLER
# ====== Broker 标识 ======
broker.id=1
# ====== 监听器配置 ======
# KRaft 模式必须配置 listeners
# CONTROLLER: Controller 节点之间的 Raft 通信
# PLAINTEXT: 客户端连接
listeners=CONTROLLER://192.168.1.101:9093,PLAINTEXT://192.168.1.101:9092
# 安全协议映射
listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
# Broker 间数据复制的监听器
inter.broker.listener.name=PLAINTEXT
# Controller 的 advertised 地址
controller.advertised.listener.names=CONTROLLER
# ====== 数据目录 ======
# 元数据日志目录(Controller 角色使用)
metadata.log.dir=/data/kafka/kraft-combined-logs
# 数据日志目录(Broker 角色使用)
log.dirs=/data/kafka/data
# ====== 分区与副本 ======
num.partitions=3
default.replication.factor=3
min.insync.replicas=2
# ====== 日志保留 ======
log.retention.hours=168
log.segment.bytes=1073741824
log.retention.check.interval.ms=300000
# ====== 性能调优 ======
# 网络线程数
num.network.threads=8
# IO 线程数
num.io.threads=8
# 发送缓冲区
socket.send.buffer.bytes=102400
# 接收缓冲区
socket.receive.buffer.bytes=102400
# 请求最大字节数
socket.request.max.bytes=104857600
# ====== Controller 专用调优 ======
# 选举超时(毫秒)——Controller 心跳超时后触发选举
controller.quorum.election.timeout.ms=1000
# Fetch 超时(毫秒)——Follower 从 Leader 拉取元数据日志的超时
controller.quorum.fetch.timeout.ms=2000
# 元数据快照最大间隔字节数
metadata.max.snapshot.interval.bytes=524288000
Node 2 和 Node 3 的配置类似,只需修改 node.id、broker.id、IP 地址即可。
4.5 启动集群
# 启动所有节点
bin/kafka-server-start.sh -daemon config/kraft/server1.properties
bin/kafka-server-start.sh -daemon config/kraft/server2.properties
bin/kafka-server-start.sh -daemon config/kraft/server3.properties
# 查看日志确认启动成功
tail -f /data/kafka/kraft-combined-logs/server.log | grep -E "started|error|ERROR"
# 验证集群状态
bin/kafka-metadata.sh --snapshot --command "cluster"
4.6 创建 Topic 并验证
# 创建测试 Topic
bin/kafka-topics.sh --bootstrap-server 192.168.1.101:9092 \
--create --topic order-events \
--partitions 12 --replication-factor 3
# 查看 Topic 详情
bin/kafka-topics.sh --bootstrap-server 192.168.1.101:9092 \
--describe --topic order-events
# 输出示例:
# Topic: order-events PartitionCount: 12 ReplicationFactor: 3
# Topic: order-events Partition: 0 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3
# Topic: order-events Partition: 1 Leader: 2 Replicas: 2,3,1 Isr: 2,3,1
# ...
# 生产消息
echo -e "order-001\norder-002\norder-003" | \
bin/kafka-console-producer.sh --bootstrap-server 192.168.1.101:9092 \
--topic order-events
# 消费消息
bin/kafka-console-consumer.sh --bootstrap-server 192.168.1.101:9092 \
--topic order-events --from-beginning
五、核心原理深入
5.1 元数据快照(Metadata Snapshot)
随着集群运行,Metadata Log 不断增长。如果不做处理,新 Controller 加入时需要从头回放所有日志——这在百万分区的场景下可能需要数小时。
KRaft 通过 快照机制 解决这个问题:
Metadata Log: [Record 1] → [Record 2] → ... → [Record 10000] → [Record 10001] → ...
↑
Snapshot #1 (包含 Record 1-10000 的状态)
↑
Snapshot #2 (包含 Record 1-50000 的状态)
快照触发条件:
- 日志条目数超过
metadata.max.snapshot.interval.bytes(默认 512MB) - 两次快照之间的时间间隔(自适应)
快照文件结构:
/data/kafka/kraft-combined-logs/
__cluster_metadata-0/
00000000000000000000.log # 元数据日志文件
00000000000000000000.snapshot # 快照文件
00000000000000000010.checkpoint # 检查点文件
当新 Controller 加入 Quorum 时,它只需要:
- 从 Leader 获取最新的快照
- 回放缓存快照中的状态到内存
- 只需重放快照之后的新日志条目
5.2 元数据传播机制
在 KRaft 模式下,Broker 如何获取最新的元数据?
Metadata Update (增量)
Controller ───────────────────────────────→ Broker
←─────────────────────────────────
Metadata Request (定期拉取)
Broker 定期向 Active Controller 发送 MetadataRequest,Controller 返回增量更新。这个过程类似于 HTTP 的条件请求(If-None-Match/ETag),避免了全量传输。
关键参数:
metadata.max.idle.ms:Broker 多久拉取一次元数据(默认 300 秒,生产环境建议 30-60 秒)metadata.max.retention.ms:Broker 缓存元数据的最大保留时间
5.3 Controller 故障切换
KRaft 模式下 Controller 故障切换的速度是其最大亮点之一:
时间线:
T0: Controller-1 (Active) 心跳正常
T1: Controller-1 网络分区,心跳停止
T2: 1000ms 后(election.timeout.ms),Controller-2 和 Controller-3 发起选举
T3: Controller-2 获得 majority 投票,成为新 Active Controller
T4: Controller-2 从 Quorum 同步最新元数据日志
T5: Broker 感知到新 Controller,开始拉取元数据更新
T6: 集群恢复服务
总耗时:约 5-15 秒(vs ZK 模式的 30-60 秒)
六、生产环境最佳实践
6.1 Controller 与 Broker 角色分离
对于大规模生产集群,建议将 Controller 和 Broker 角色分离到不同物理节点:
# 纯 Controller 节点配置
process.roles=controller
node.id=1
controller.quorum.voters=1@ctrl-1:9093,2@ctrl-2:9093,3@ctrl-3:9093
listeners=CONTROLLER://ctrl-1:9093
controller.listener.names=CONTROLLER
metadata.log.dir=/data/kafka/controller-logs
# 纯 Broker 节点配置
process.roles=broker
node.id=101
controller.quorum.voters=1@ctrl-1:9093,2@ctrl-2:9093,3@ctrl-3:9093
listeners=PLAINTEXT://broker-1:9092
metadata.log.dir=/data/kafka/broker-logs
为什么分离?
- 故障域隔离:Broker 压力大(大量 IO)可能影响 Controller 响应,分离后互不干扰
- 资源优化:Controller 不需要大磁盘,但需要稳定网络;Broker 需要大磁盘
- 扩缩灵活:可以独立扩容 Broker 节点而不影响 Quorum 稳定性
6.2 Quorum 大小选择
| 集群规模 | 推荐 Quorum 大小 | 理由 |
|---|---|---|
| <10 Broker | 3 | 最小 Quorum, tolerate 1 故障 |
| 10-50 Broker | 3 或 5 | 5 可以 tolerate 2 故障 |
| >50 Broker | 5 或 7 | 大集群 Controller 故障影响大,需要更高容错 |
6.3 关键性能调优参数
# ====== 元数据相关 ======
# Broker 元数据刷新间隔(降低此值可以加快故障感知)
metadata.max.idle.ms=30000 # 默认 300 秒,生产建议 30 秒
# 元数据快照间隔(影响 Controller 恢复速度)
metadata.max.snapshot.interval.bytes=209715200 # 200MB,更频繁的快照
# ====== Controller Quorum ======
# 选举超时(不要太大,否则故障恢复慢)
controller.quorum.election.timeout.ms=1000 # 1 秒
# Fetch 超时
controller.quorum.fetch.timeout.ms=2000 # 2 秒
# ====== Broker 数据面 ======
# 副本同步超时
replica.lag.time.max.ms=30000 # 30 秒
# 最小同步副本数(重要!保证数据不丢失)
min.insync.replicas=2 # 配合 acks=all 使用
# ====== 网络与 IO ======
# 批量大小
batch.size=65536 # 64KB
linger.ms=5 # 等待 5ms 收集更多消息
# 缓冲区内存
buffer.memory=67108864 # 64MB
6.4 监控指标
KRaft 模式下需要关注的关键 JMX 指标:
# Controller Quorum 健康
kafka.controller:type=KRaftController,name=ActiveControllerCount
kafka.controller:type=KRaftController,name=QuorumState
# 元数据日志
kafka.controller:type=MetadataQuorum,name=MaxObservedTimestamp
kafka.controller:type=MetadataQuorum,name=LogEndOffset
# Broker 元数据
kafka.server:type=BrokerMetadata,name=ActiveTopicsCount
kafka.server:type=BrokerMetadata,name=PartitionCount
# Raft 复制
kafka.raft:type=RaftManager,name=CommitCount
kafka.raft:type=RaftManager,name=SnapshotCount
七、Java 客户端实战
7.1 生产者:精确一次语义
Properties props = new Properties();
props.put("bootstrap.servers", "192.168.1.101:9092,192.168.1.102:9092,192.168.1.103:9092");
// ========== 幂等生产者(精确一次,单分区维度) ==========
props.put("enable.idempotence", "true"); // 启用幂等
props.put("acks", "all"); // 等待所有 ISR 确认
props.put("retries", Integer.MAX_VALUE); // 无限重试
props.put("max.in.flight.requests.per.connection", 5); // 幂等模式下最大 5
props.put("compression.type", "lz4"); // LZ4 压缩
// ========== 事务生产者(精确一次,跨分区维度) ==========
props.put("transactional.id", "order-service-txn-1");
KafkaProducer<String, String> producer = new KafkaProducer<>(props, new StringSerializer(), new StringSerializer());
// 事务发送示例
producer.initTransactions();
try {
producer.beginTransaction();
// 发送到多个 Topic
producer.send(new ProducerRecord<>("order-events", "order-001", "{\"product\":\"iPhone\"}"));
producer.send(new ProducerRecord<>("inventory-events", "order-001", "{\"sku\":\"iphone-16\",\"qty\":-1}"));
producer.commitTransaction();
} catch (ProducerFencedException | AuthorizationException e) {
// 不可恢复的错误,关闭生产者
producer.close();
} catch (KafkaException e) {
// 可恢复的错误,回滚事务
producer.abortTransaction();
}
7.2 消费者:手动提交 + 重平衡监听
Properties props = new Properties();
props.put("bootstrap.servers", "192.168.1.101:9092");
props.put("group.id", "order-processor-group");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// ========== 关键消费配置 ==========
props.put("enable.auto.commit", "false"); // 禁用自动提交
props.put("auto.offset.reset", "earliest"); // 从最早开始
props.put("max.poll.records", "500"); // 每次拉取 500 条
props.put("max.poll.interval.ms", "300000"); // 处理超时 5 分钟
props.put("session.timeout.ms", "10000"); // 会话超时 10 秒
props.put("heartbeat.interval.ms", "3000"); // 心跳间隔 3 秒
props.put("isolation.level", "read_committed"); // 只读已提交的事务消息
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("order-events"), new ConsumerRebalanceListener() {
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
// 分区被回收前,提交当前 offset
consumer.commitSync();
}
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
// 分区分配后,可以做一些初始化工作
System.out.println("Assigned partitions: " + partitions);
}
});
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
processOrder(record.value()); // 业务处理
}
// 手动提交 offset
consumer.commitSync();
}
} finally {
consumer.close();
}
7.3 Spring Boot 集成
# application.yml
spring:
kafka:
bootstrap-servers: 192.168.1.101:9092,192.168.1.102:9092,192.168.1.103:9092
producer:
acks: all
retries: 2147483647
batch-size: 32768
buffer-memory: 67108864
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
properties:
enable.idempotence: true
compression.type: lz4
consumer:
group-id: order-processor-group
enable-auto-commit: false
auto-offset-reset: earliest
max-poll-records: 500
properties:
isolation.level: read_committed
listener:
ack-mode: MANUAL_IMMEDIATE
@Service
public class OrderEventConsumer {
@KafkaListener(topics = "order-events", groupId = "order-processor-group")
public void handleOrder(ConsumerRecord<String, String> record, Acknowledgment ack) {
try {
// 业务逻辑处理
OrderDTO order = JsonUtils.fromJson(record.value(), OrderDTO.class);
orderService.process(order);
// 处理成功,手动确认
ack.acknowledge();
} catch (Exception e) {
log.error("处理订单失败: key={}, value={}", record.key(), record.value(), e);
// 不确认,等待重试
throw new RuntimeException("订单处理失败", e);
}
}
}
八、从 ZK 模式迁移到 KRaft 模式
重要提醒:Kafka 3.7 开始,社区已移除从 ZK 到 KRaft 的在线迁移工具(
kafka-storage.sh migrate-metadata)。如果你的集群还在 ZK 模式,需要通过"双写双读 + 流量切换"的方式迁移。
8.1 迁移策略:影子集群方案
阶段 1: 部署 KRaft 集群(影子模式)
- 新建 3 节点 KRaft 集群
- 使用 MirrorMaker 2.0 同步数据
- 消费者双读(主 ZK + 验证 KRaft)
阶段 2: 生产者双写
- 生产者同时写入 ZK 集群和 KRaft 集群
- 验证数据一致性
阶段 3: 流量切换
- 消费者切换到 KRaft 集群
- 生产者停止写入 ZK 集群
阶段 4: 下线 ZK 集群
- 确认数据完全迁移后,下线 ZK
8.2 MirrorMaker 2.0 配置
# mm2.properties
clusters = zk-cluster, kraft-cluster
zk-cluster.bootstrap.servers = 192.168.1.201:9092
kraft-cluster.bootstrap.servers = 192.168.1.101:9092
# 同步方向
sync.topic.configs.enabled = true
sync.topic.acls.enabled = true
emit.checkpoints.enabled = true
emit.breadcrumbs.enabled = true
replication.policy.class = org.apache.kafka.connect.mirror.IdentityReplicationPolicy
# 启动 MirrorMaker
bin/connect-mirror-maker.sh mm2.properties
8.3 数据一致性验证
# 使用 kafka-consumer-groups.sh 检查消费进度
bin/kafka-consumer-groups.sh --bootstrap-server 192.168.1.101:9092 \
--describe --group order-processor-group
# 对比两个集群的 Topic 分区详情
bin/kafka-topics.sh --bootstrap-server 192.168.1.201:9092 --describe --topic order-events
bin/kafka-topics.sh --bootstrap-server 192.168.1.101:9092 --describe --topic order-events
九、常见问题排查
9.1 Controller 无法选举
现象:集群启动后,Broker 无法连接,日志显示 "Controller not available"
# 检查 Quorum 配置是否正确
bin/kafka-metadata.sh --snapshot --command "cluster"
# 检查 Controller 节点之间的网络连通性
# 确保 controller.quorum.voters 中的地址和端口正确
# 确保 controller.listener.names 与 listeners 中的名称匹配
# 常见错误配置:
# ❌ process.roles=broker,controller 但缺少 CONTROLLER listener
# ❌ controller.quorum.voters 中的 ID 与 node.id 不匹配
# ❌ controller.listener.names 未配置或名称不一致
9.2 Broker 无法加入集群
现象:新 Broker 启动后一直无法注册到集群
# 检查日志
tail -f /data/kafka/kraft-combined-logs/server.log | grep -i "register\|broker"
# 常见原因:
# 1. node.id 与 Quorum 中已存在的 ID 冲突
# 2. controller.quorum.voters 配置错误(新 Broker 没有正确指向 Quorum)
# 3. 纯 Broker 节点的 process.roles=broker,但 controller.quorum.voters 没有配置
9.3 元数据不一致
# 强制重新加载元数据
bin/kafka-metadata.sh --snapshot --command "broker" --broker-id 1
# 如果出现严重不一致,可能需要重新格式化(谨慎!会丢失数据)
# 仅在测试环境使用
十、总结与展望
10.1 核心要点回顾
- KRaft 是 Kafka 架构的代际升级,不是简单的 ZK 替换,而是将元数据管理内化为 Kafka 的核心能力
- Raft 协议赋予了 Controller Quorum 强一致性和快速故障恢复(5-15 秒 vs 30-60 秒)
- 百万级分区不再是纸上谈兵,KRaft 的 Metadata Log + 快照机制让元数据扩展性提升了 10 倍
- 运维复杂度大幅降低,一套系统搞定一切,告别"Kafka + ZooKeeper"双系统运维
- 2026 年新集群必须用 KRaft,ZK 模式已进入维护末期
10.2 未来方向
- Tiered Storage(分层存储):KRaft 模式下更容易实现冷热数据分层,将老数据自动迁移到 S3/OSS
- KIP-848(消费者组重平衡协议重写):新一代消费者组协议将进一步提升重平衡效率
- 元数据 API 扩展:KRaft 为未来元数据层面的创新(如动态配置、多集群联邦)奠定了基础
对于正在规划 Kafka 集群的团队来说,KRaft 不是"要不要选"的问题,而是"怎么用好"的问题。开始吧——告别 ZooKeeper,拥抱新一代 Kafka。