在前两篇文章中,我们从“为什么移除 ZooKeeper”讲到了 KRaft 的内部架构——元数据作为一条 Raft 复制的日志,由 Broker 消费对齐。但理解架构是一回事,把一套正在承载业务的 ZooKeeper 模式集群安全迁移到 KRaft,是另一回事。迁移的核心挑战不在于“能不能迁”,而在于在迁移过程中,业务不中断、元数据不丢失、并且始终保留一条可回退的路径。KRaft 的迁移设计正是围绕这个目标构建的:ZooKeeper 在迁移期间始终保持权威,直到你主动放弃它。 本文从迁移的硬性前置条件讲起,拆解三阶段迁移流程,重点剖析 dual-write 阶段为什么是“可回滚窗口”,以及回滚失效的精确边界。
一、前置条件:三条必须满足的硬约束
迁移不是“改一个配置、重启一下”就能完成的操作。在动手之前,有三条硬约束必须满足。
1.1 版本路径:不能跳过 3.x 直接上 4.0
KRaft-only 是 Kafka 4.0 的硬性要求,但迁移本身必须在 3.x 上完成。正确的路径是:
text
3.x(ZooKeeper 模式) → 3.9.x(桥接版本,支持双模式) → 完成 ZK→KRaft 迁移 → 稳定运行 → 升级到 4.0
跳过 3.x 直接升级到 4.0 是行不通的——4.0 已经没有 ZooKeeper 模式,也没有接收 ZooKeeper 元数据的代码路径。3.9.x 是唯一的桥接版本,它同时支持 ZooKeeper 模式和 KRaft 模式,是迁移的起点。
1.2 Controller Quorum:独立、稳定、奇数
Quorum 必须是奇数个节点:3 个容忍 1 台故障,5 个容忍 2 台。生产环境至少 3 个。
Controller 应独立部署(dedicated mode) ,而不是与 Broker 合并(combined mode)。Controller 的元数据日志对磁盘延迟非常敏感,不要与其他高 I/O 服务混跑。Combined 模式仅适合小集群和测试环境——在生产环境中,Controller 的 Raft 日志写入和 Broker 的消息写入争用同一份磁盘 I/O,元数据变更的延迟会直接传导到业务消息的稳定性上。
1.3 存储目录:format 会清空目标目录
Controller 和 Broker 各自需要独立的 log.dirs。kafka-storage format 操作会清空目标目录——如果选错目录,意味着数据丢失。在迁移前,必须确认 Controller 的存储目录是空目录或专用目录,绝对不能指向 Broker 的数据目录。
二、三阶段迁移流程
迁移的执行分为三个阶段,每个阶段的职责和检查点都很明确。
2.1 阶段一:准备 Controller Quorum
目标:让 Controller Quorum 就绪,但不接管任何权威。
步骤:
生成集群 ID:kafka-storage random-uuid。这个 ID 用于标识整个集群,Controller 和 Broker 必须使用同一个。
编写 Controller 配置:process.roles=controller、node.id、controller.quorum.voters(枚举全部 Controller)、独立的 log.dirs。
格式化 Controller 存储:kafka-storage format --cluster-id --config controller.properties。这一步会清空目标目录。
启动 Controller 节点。此时 Quorum 已经形成,但ZooKeeper 仍然是元数据的权威来源,Controller 只是在等待迁移指令。
2.2 阶段二:Dual-write(可回滚窗口)
这是整个迁移过程中最关键、也最需要谨慎的阶段。
操作:
为每个 Broker 添加 controller.quorum.voters 配置,指向 Controller Quorum,保持 process.roles=broker。
逐个滚动重启 Broker。重启后,Broker 进入迁移模式(zookeeper.metadata.migration.enable=true)。
Active Controller 从 ZooKeeper 读取现有元数据,一次性迁移到 KRaft 日志,然后对后续的元数据变更同时写入 ZooKeeper 和 KRaft。
这个阶段的核心特征是:ZooKeeper 仍然是权威,KRaft 是镜像。所有元数据变更同时写入两套系统,但 Broker 仍然从 ZooKeeper 读取权威元数据。
为什么这是“可回滚窗口” :因为 ZooKeeper 仍然持有完整的、当前的元数据,你可以随时把 Broker 回退到 ZooKeeper 模式,而不丢失任何元数据变更。回滚的操作是:将 Controller 的迁移标志关闭(zookeeper.metadata.migration.enable=false),让 Controller 退出迁移模式,Broker 继续从 ZooKeeper 读取元数据。
回滚的安全前提:回滚只发生在 Controller 退出迁移模式之前。一旦 Controller 停止 dual-write,ZooKeeper 就不再被更新,回滚将丢失之后的元数据变更。
2.3 阶段三:切换权威与移除 ZooKeeper
这是“不可逆”的起点。
操作:
验证元数据一致性:在 dual-write 阶段,对比 ZooKeeper 和 KRaft 中的元数据(Topic 数量、分区分配、ACL 等),确认两者一致后再执行切换。
将 Controller 切换到权威模式:Controller 停止向 ZooKeeper 写入,KRaft 成为元数据的唯一权威。
逐个滚动重启 Broker:设置 process.roles=broker,移除 zookeeper.connect 配置,移除迁移标志。Broker 现在只从 KRaft 日志读取元数据。
下线 ZooKeeper:Controller 退出迁移模式后,ZooKeeper 不再被使用,可以安全关闭并移除。
不可逆的边界:一旦进入“MoveToKRaftControllerOnly”状态(Controller 停止 dual-write),ZooKeeper 元数据不再权威,集群无法回到 ZooKeeper 模式。
三、迁移期间的监控与常见故障
3.1 监控命令
kafka-metadata-quorum describe --status 是迁移期间最重要的观测工具。它告诉你:当前 Quorum 的 Leader 是谁、Follower 是否健康、迁移进行到了哪一步。
关注三个信号:
Leader 稳定性:如果 Leader 频繁切换,说明 Quorum 的网络或磁盘有问题,迁移可能卡住。
Follower 的滞后:Follower 如果持续落后,说明元数据复制没有跟上,切换时可能丢失元数据。
迁移进度:确认 Broker 是否全部进入了迁移模式。
3.2 一个真实的迁移风险:ZK Leader 故障
在 dual-write 阶段,如果 ZooKeeper 的 Leader 发生故障切换,迁移可能被阻塞。Controller 需要从 ZK 读取元数据变更,ZK 的 Leader 切换会导致短暂的不可用,Controller 收不到确认,迁移进度卡住。这不是一个罕见场景——大规模 ZK 集群的 Leader 切换是正常运维事件,但如果恰好发生在迁移窗口内,就会延长迁移时间。
3.3 回滚后的一个隐蔽问题:Controller Epoch 不匹配
如果在 dual-write 阶段执行了回滚,KRaft 的 Controller Epoch 可能已经超过了 ZooKeeper 的 Controller Epoch。这会导致回滚后出现离线分区——因为 ZooKeeper 的 Controller 认为自己的 Epoch 已经过期。正确的回滚流程需要在回滚之前验证 ZooKeeper 的 Controller Epoch 大于 KRaft 的 Epoch,否则需要先处理 Epoch 冲突。
四、迁移检查清单
五、小结
三条硬约束:必须经过 3.9.x 桥接版本,不能从 3.x 直接跳 4.0;Controller Quorum 必须奇数、独立部署、独立磁盘;kafka-storage format 会清空目标目录,存储规划必须在操作前完成。
三阶段流程:准备 Quorum → Dual-write(可回滚窗口)→ 切换权威并移除 ZooKeeper。
Dual-write 的本质:ZooKeeper 仍是权威,KRaft 是镜像,元数据双写。回滚窗口仅存在于 Controller 退出迁移模式之前。
不可逆的边界:Controller 停止 dual-write 的那一刻,ZooKeeper 元数据不再权威,无法回到 ZooKeeper 模式。
监控与故障:kafka-metadata-quorum 是迁移期间的核心观测工具;ZK Leader 故障可能阻塞迁移;回滚后需验证 Controller Epoch 以避免离线分区。