bigywater.tech
ZooKeeper · 104

ZooKeeper 迁移到 KRaft

ZooKeeper 迁移到 KRaft 是 Kafka 3.x → 4.0 的必经之路。这篇不转述文档,而是真的把一个在线集群迁一遍:先起 KRaft controller,滚动重启 broker 触发元数据迁移,再把 broker 切成纯 KRaft,最后停掉 ZooKeeper,全程有生产者持续写入,事后核对数据与元数据。

0 / 0
迁移全程 2702 条消息的丢失 / 重复数
1.0 s
迁移全程生产者相邻两次确认的最长间隔
DUAL_WRITE
迁移期间 KRaft controller 同时写 ZooKeeper,可以回滚

起点

实测 · 本机执行输出
  主题: [('__consumer_offsets', '50', '3'), ('mig', '3', '3'), ('t1', '3', '3'), ('t2', '3', '3')]
  消费组 g1 的 t1 各分区 CURRENT-OFFSET: [('2', '41'), ('1', '159'), ('0', '0')]
  后台生产者已写入 60 条(每 20 ms 一条,同步等待 acks=all)
  ZooKeeper 状态: {'controller': '{"version":2,"brokerid":1,"timestamp":"1790833827967","kraftControllerEpoch":-1}', 'migration': False, 'brokers': ['1', '2', '3']}

起点的 ZooKeeper 节点见上一篇(Kafka 在 ZooKeeper 里存什么)。

第一步:KRaft controller

实测 · 本机执行输出
  [2026-10-01 13:50:44,360] WARN inter.broker.protocol.version is deprecated in KRaft mode as of 3.3 and will only be read when first upgrading from a KRaft prior to 3.3. See kafka-storage.sh help for d
  controller 日志里与迁移有关的行:
    INFO [ZooKeeperClient KRaft Migration] Initializing a new session to 127.0.0.1:7300. (kafka.zookeeper.ZooKeeperClient)
    INFO [ZooKeeperClient KRaft Migration] Waiting until connected. (kafka.zookeeper.ZooKeeperClient)
    INFO [ZooKeeperClient KRaft Migration] Connected. (kafka.zookeeper.ZooKeeperClient)
    INFO [ControllerRegistrationManager id=3000 incarnation=IKnKCIK7Say2VAOXu6F0ZA] sendControllerRegistration: attempting to send ControllerRegistrationRequestData(controllerId=3000, incarnationId=IKnKCIK7Say2VAOX
    INFO [KRaftMigrationDriver id=3000] Initial migration of ZK metadata is not done. (org.apache.kafka.metadata.migration.KRaftMigrationDriver)
    INFO [KRaftMigrationDriver id=3000] 3000 transitioning from UNINITIALIZED to INACTIVE state (org.apache.kafka.metadata.migration.KRaftMigrationDriver)
    INFO [MetadataLoader id=3000] InitializeNewPublishers: initializing KRaftMigrationDriver with a snapshot at offset 4 (org.apache.kafka.image.loader.MetadataLoader)
    INFO [KRaftMigrationDriver id=3000] Became active migration driver in 38508458 ns. Transitioned migration state from ZkMigrationLeadershipState{kraftControllerId=-1, kraftControllerEpoch=-1, kraftMetadataOffset
  此时 ZooKeeper: {'controller': '{"version":2,"brokerid":1,"timestamp":"1790833827967","kraftControllerEpoch":-1}', 'migration': True, 'brokers': ['1', '2', '3']}

第二步:broker 带迁移开关滚动重启

实测 · 本机执行输出
  broker 1 已用迁移配置重启(6 s)
  broker 2 已用迁移配置重启(11 s)
  broker 3 已用迁移配置重启(17 s)
  controller 日志里迁移相关的行(最后 10 行):
    INFO [KRaftMigrationDriver id=3000] Completed migration of metadata from ZooKeeper to KRaft. 66 records were generated in 100 ms across 1 batches. The average time spent waiting on a batch was 26.00 ms. The record types were {TOPI
    INFO [KRaftMigrationDriver id=3000] Finished initial migration of ZK metadata to KRaft in 3311791 ns. Transitioned migration state from ZkMigrationLeadershipState{kraftControllerId=3000, kraftControllerEpoch=1, kraftMetadataOffset
    INFO [KRaftMigrationDriver id=3000] 3000 transitioning from ZK_MIGRATION to SYNC_KRAFT_TO_ZK state (org.apache.kafka.metadata.migration.KRaftMigrationDriver)
    INFO [KRaftMigrationDriver id=3000] Performing a full metadata sync from KRaft to ZK. (org.apache.kafka.metadata.migration.KRaftMigrationDriver)
    INFO [KRaftMigrationDriver id=3000] Did not make any ZK writes when reconciling with KRaft state. (org.apache.kafka.metadata.migration.KRaftMigrationDriver)
    INFO [KRaftMigrationDriver id=3000] 3000 transitioning from SYNC_KRAFT_TO_ZK to KRAFT_CONTROLLER_TO_BROKER_COMM state (org.apache.kafka.metadata.migration.KRaftMigrationDriver)
    INFO [KRaftMigrationDriver id=3000] Expected driver state SYNC_KRAFT_TO_ZK but found KRAFT_CONTROLLER_TO_BROKER_COMM. Not running this event SyncKRaftMetadataEvent. (org.apache.kafka.metadata.migration.KRaftMigrationDriver)
    INFO [KRaftMigrationDriver id=3000] Sending RPCs to broker before moving to dual-write mode using at offset and epoch OffsetAndEpoch(offset=117, epoch=1) (org.apache.kafka.metadata.migration.KRaftMigrationDriver)
    INFO [KRaftMigrationDriver id=3000] 3000 transitioning from KRAFT_CONTROLLER_TO_BROKER_COMM to DUAL_WRITE state (org.apache.kafka.metadata.migration.KRaftMigrationDriver)
    INFO [KRaftMigrationDriver id=3000] Expected driver state KRAFT_CONTROLLER_TO_BROKER_COMM but found DUAL_WRITE. Not running this event SendRPCsToBrokersEvent. (org.apache.kafka.metadata.migration.KRaftMigrationDriver)
  ZooKeeper 状态: {'controller': '{"version":2,"brokerid":3000,"timestamp":"1790833864954","kraftControllerEpoch":1}', 'migration': True, 'brokers': ['1', '2', '3']}
  此时 /brokers/topics 仍是: ['__consumer_offsets', 'mig', 't1', 't2'] (controller 已进入 DUAL_WRITE:KRaft 的元数据变更同时写回 ZooKeeper,迁移未完成前还能回滚)
  kafka-metadata-quorum describe --status:
    ClusterId:              sgR5w7tVSz24GYvEk_UErQ
    LeaderId:               3000
    LeaderEpoch:            1
    HighWatermark:          141
    MaxFollowerLag:         0
    MaxFollowerLagTimeMs:   0
    CurrentVoters:          [{"id": 3000, "directoryId": null, "endpoints": ["CONTROLLER://127.0.0.1:7910"]}]
    CurrentObservers:       [{"id": 3, "directoryId": "YHxOXhDv16rQGDlOgGJJbw"}, {"id": 1, "directoryId": "xsNmW3trQLSzh7VJGsD_JA"}, {"id": 2, "directoryId": "9Z0RWHLrU2XVvIwloNjnmA"}]

每个 broker 重启期间,生产者会遇到 leader 切换(和controller 被杀类似),重试机制把它们吸收了。

第三步:切成纯 KRaft

实测 · 本机执行输出
  broker 1 以 KRaft 模式重启
  broker 2 以 KRaft 模式重启
  broker 3 以 KRaft 模式重启
  ZooKeeper 状态: {'controller': '{"version":2,"brokerid":3000,"timestamp":"1790833864954","kraftControllerEpoch":1}', 'migration': True, 'brokers': []}

第四步:收尾

实测 · 本机执行输出
  ZooKeeper 已停止;后台生产者继续写: 143 条/4 s
  生产者总共写入 2702 条(同步确认),过程中出现的发送错误类型 {}
  迁移后的主题: [('__consumer_offsets', '50', '3'), ('mig', '3', '3'), ('t1', '3', '3'), ('t2', '3', '3')] | 与迁移前一致: True
  消费组 g1 的 t1 各分区 CURRENT-OFFSET: [('2', '41'), ('1', '159'), ('0', '0')] | 与迁移前一致: True
  主题 mig 读回 2702 条:丢失(同步确认过的 id 里没读到)0 条,重复 0 条
  生产者相邻两次确认之间的最长间隔 1.0 s(整个迁移过程;超过 1 s 的间隔共 1 个)

这一步之后 ZooKeeper 完全没有参与。我没有测回滚路径,也没有核对文档里这一步之后是否还能回滚。

分步演示

ZooKeeper → KRaft
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / ZooKeeper 3.9.6(brew 安装的 jar,用 java -cp 直接启动自己的实例,OpenJDK 17 Temurin,JVM 堆 128 MiB)/ 本机回环端口 7300+,数据在临时目录;默认 tickTime=2000;计时用 time.perf_counter,数字每次运行会有小幅抖动。Kafka 3.9.2(自带 ZooKeeper 3.8.4 客户端);3 个 broker 端口 7901–7903,KRaft controller 单节点 7910(生产环境应为 3 节点以上的投票);zookeeper.session.timeout.ms=6000;生产者 kafka-python-ng,acks=all、同步发送
    • 没有验证:迁移前后的回滚、3 节点 controller 投票、有 ACL / 配额 / SCRAM 的集群、大量分区的迁移耗时、事务与幂等生产者在迁移中的行为、Kafka 3.9 之前版本的迁移(要求 3.4+)
    京ICP备2026067502号-1京公网安备11010502063487号