1. 为什么值得花时间啃透 Kafka Raft
如果你正在维护一套依赖 ZooKeeper 的 Kafka 集群,或者正准备把老集群升级到新版本,那Kafka Raft(业内也常叫它 KRaft)这个词你一定绕不过去。我从两年前开始跟进这个方向,先后在测试环境和几个中小规模生产环境里做过迁移验证,踩过的坑不算少。这篇文章不是官方文档的翻译,而是把我自己整理的知识脉络、实操记录和排查经验一次性摊开讲清楚。
先说清楚它到底是什么。Kafka Raft 是 Kafka 用来替代 ZooKeeper 的一套元数据管理共识机制。在旧架构里,Kafka 只负责消息的存储和收发,而集群有哪些 Broker、每个 Topic 有多少分区、分区的 Leader 是谁、副本怎么分配、ACL 配置是什么,这些全部存在 ZooKeeper 里。新架构把这一整套元数据管理收回到 Kafka 自己内部,用 Raft 共识算法在 Controller 节点之间同步元数据。换句话说,Kafka 从"半自管理"变成了"完全自管理"。
它能解决什么问题?最直接的三个:部署复杂度下降、元数据操作延迟降低、集群规模上限提高。以前你要维护两套分布式系统,ZooKeeper 有它自己的运维逻辑、监控指标、调优参数,现在只剩一套。元数据变更不再需要跨系统同步,Controller 故障切换从分钟级压缩到秒级。分区数量从几万级别可以往上探到百万级别。
适合谁来读?如果你是 Kafka 运维、后端开发、或者正在做消息中间件选型,这篇内容都能用得上。我会从架构原理讲到参数配置,再到实际迁移步骤和故障排查,尽量让刚接触的人能看懂,也让有经验的人能拿到可以直接抄的配置。
2. 架构拆解:KRaft 到底把 ZooKeeper 的活怎么接了
2.1 旧架构的痛点到底在哪
要理解 KRaft 的价值,得先看清楚 ZooKeeper 架构的瓶颈。在旧模式里,Kafka Controller 是集群的"大脑",但它这个大脑是单点的。Controller 挂了之后,需要从 ZooKeeper 里重新选举一个 Controller,这个过程涉及 ZooKeeper 的 Watcher 通知、多个 Broker 竞争注册,实测下来通常要几秒到几十秒。集群规模越大、分区越多,这个切换时间越长,因为新 Controller 要把全量元数据从 ZooKeeper 拉回来重建内存状态。
另一个问题是元数据写入路径太长。创建一个 Topic,请求先到 Controller,Controller 写到 ZooKeeper,ZooKeeper 再通知所有 Broker 去拉取更新。这条链路上任何一环抖动,都会让元数据操作变慢。我见过一个集群分区数到二十万之后,创建一个 Topic 要等十几秒,运维同学每次操作都得屏住呼吸。
还有一个隐性成本是双系统运维。ZooKeeper 有自己的选举机制、自己的磁盘要求、自己的监控体系。你得同时盯着两套系统的健康状态,排查问题时还要判断到底是 Kafka 的问题还是 ZooKeeper 的问题。这种心智负担在故障时刻特别要命。
2.2 Raft 共识在 Kafka 里怎么落地
KRaft 的核心思路是把元数据当成一个有序的日志来管理。所有元数据变更——建 Topic、改分区、Broker 上下线——都作为一条条记录追加到这个日志里。Raft 算法保证这个日志在多个 Controller 节点之间保持一致,只要多数节点确认写入,这条记录就算提交成功。
这里有几个关键角色需要分清:
- Controller 节点:参与 Raft 选举和日志复制的节点,负责处理元数据变更。它们同时也是 Broker,可以收发消息。
- Leader Controller:当前处理元数据写入的主节点,所有变更请求都先到它这里。
- Follower Controller:从 Leader 同步日志,Leader 挂了之后参与新 Leader 选举。
- Broker 节点:只负责消息存储和收发,不参与元数据共识,但会从 Controller 拉取元数据快照。
- Active Controller:在旧架构里叫 Controller,在 KRaft 里就是 Raft Leader,概念上对应。
元数据日志的复制用的是 Raft 的标准流程:Leader 收到变更请求,追加到本地日志,然后并行发给所有 Follower。多数 Follower 确认写入后,Leader 提交这条记录并应用到状态机,同时通知 Broker 更新。整个过程在局域网内通常几毫秒到几十毫秒完成。
注意:KRaft 里的 Controller 数量必须是奇数,推荐 3 个或 5 个。3 个能容忍 1 个故障,5 个能容忍 2 个。不要用偶数,因为偶数在脑裂场景下无法形成多数派,反而降低可用性。
2.3 元数据快照与日志压缩机制
Raft 日志不能无限增长,否则重启时回放会非常慢。KRaft 用了快照加日志压缩的机制。当日志积累到一定条数(由metadata.log.max.record.bytes.between.snapshots控制,默认 20MB 左右),Controller 会把当前全量元数据打成一个快照文件,然后截断旧的日志段。
快照文件存在 Controller 节点的metadata.log.dir目录下,命名类似snapshot_<offset>。重启时,Controller 先加载最新的快照,再回放快照之后的日志,这样恢复速度就快很多。我实测过一个有五十万分区的集群,全量日志回放要几分钟,但加载快照加少量日志回放只要几秒。
这里有个容易忽略的点:快照生成是异步的,不会阻塞正常的元数据写入。但如果快照生成太频繁,会占用磁盘 IO。默认配置下一般不用调,除非你的元数据变更极其频繁,可以适当调大metadata.log.max.record.bytes.between.snapshots。
3. 核心配置与实操要点:从零搭一套 KRaft 集群
3.1 关键配置项逐个拆解
搭 KRaft 集群,配置文件里最重要的几个参数必须搞清楚。下面这张表是我整理的核心配置对照:
| 配置项 | 作用 | 推荐值 | 说明 |
|---|---|---|---|
process.roles | 节点角色 | broker或controller或broker,controller | 生产环境建议分离部署 |
node.id | 节点唯一 ID | 整数,集群内唯一 | 不要重复,否则启动报错 |
controller.quorum.voters | Controller 投票成员 | 1@host1:9093,2@host2:9093,3@host3:9093 | 格式为 id@host:port |
controller.listener.names | Controller 监听器名称 | CONTROLLER | 与 listeners 配置对应 |
listeners | 监听地址 | PLAINTEXT://:9092,CONTROLLER://:9093 | 注意协议前缀 |
log.dirs | 数据目录 | 独立磁盘路径 | 元数据日志和消息数据分开更好 |
metadata.log.dir | 元数据日志目录 | 独立路径 | 不配则默认在 log.dirs 下 |
process.roles这个参数决定了节点的身份。如果配成broker,controller,这个节点既参与元数据共识又存消息,适合测试环境或者小集群。生产环境我强烈建议分离部署,Controller 节点专门做共识,Broker 节点专门存消息,互不干扰。原因很简单:Controller 对延迟敏感,Broker 对吞吐和磁盘 IO 敏感,混在一起容易互相拖累。
controller.quorum.voters是 KRaft 的"通讯录",所有 Controller 节点都必须配置完全相同的列表。格式是节点ID@主机名:端口,多个用逗号分隔。这里的主机名必须能被其他节点解析到,建议用稳定的内网域名或者固定 IP,不要用会变的容器主机名。
3.2 单节点快速验证环境搭建
想快速体验 KRaft,可以先在单机上跑一个合并角色的节点。步骤如下:
第一步,生成集群 ID。KRaft 需要一个全局唯一的集群标识:
bin/kafka-storage.sh random-uuid这条命令会输出一个类似xtzWWN4bTjitpL3kfd9s5g的字符串,记下来。
第二步,格式化存储目录。这一步会初始化元数据日志:
bin/kafka-storage.sh format -t xtzWWN4bTjitpL3kfd9s5g -c config/kraft/server.properties-t后面跟集群 ID,-c指定配置文件。格式化只需要做一次,重复执行会报错。
第三步,启动节点:
bin/kafka-server-start.sh config/kraft/server.properties启动日志里会看到KRaft mode相关的信息,以及 Controller 选举完成的提示。如果看到Leader election completed之类的字样,说明启动成功。
第四步,验证。创建一个测试 Topic:
bin/kafka-topics.sh --create --topic test-kraft --partitions 3 --replication-factor 1 --bootstrap-server localhost:9092然后列出 Topic 确认:
bin/kafka-topics.sh --list --bootstrap-server localhost:9092能列出test-kraft就说明元数据管理正常工作。
提示:单节点环境只适合功能验证,没有任何容错能力。节点一挂,整个集群就不可用。生产环境至少 3 个 Controller 加 3 个 Broker。
3.3 三 Controller 加三 Broker 的生产级配置
生产环境我推荐 3 个 Controller 节点加 3 个 Broker 节点的最小高可用架构。Controller 节点配置如下:
process.roles=controller node.id=1 controller.quorum.voters=1@ctrl1:9093,2@ctrl2:9093,3@ctrl3:9093 controller.listener.names=CONTROLLER listeners=CONTROLLER://:9093 log.dirs=/data/kraft-metadata metadata.log.dir=/data/kraft-metadata三个 Controller 节点的配置只有node.id不同,分别是 1、2、3。controller.quorum.voters三个节点必须完全一致。
Broker 节点配置:
process.roles=broker node.id=101 controller.quorum.voters=1@ctrl1:9093,2@ctrl2:9093,3@ctrl3:9093 listeners=PLAINTEXT://:9092 log.dirs=/data/kafka-logsBroker 节点的node.id从 101 开始,和 Controller 的 ID 段分开,方便管理。controller.quorum.voters同样要配全三个 Controller 地址。
这里有个实操细节:先启动所有 Controller 节点,再启动 Broker 节点。Controller 之间会先完成 Leader 选举,形成可用的元数据集群。Broker 启动后会向 Controller 注册自己,拉取元数据。如果先启动 Broker,它会因为连不上 Controller 而反复重试,虽然最终也能成功,但日志里会有一堆连接错误,看着糟心。
3.4 从 ZooKeeper 迁移的实操路径
如果你已经有 ZooKeeper 集群,想迁移到 KRaft,官方提供了一条滚动迁移的路径。整个过程分几个阶段,不能跳步。
第一阶段,把 ZooKeeper 集群升级到支持迁移的版本,同时把 Kafka 集群升级到支持 KRaft 的版本。这个阶段集群还在 ZooKeeper 模式下运行,只是代码版本更新了。
第二阶段,启动一组 Controller 节点,让它们以"迁移模式"运行。这时候 Controller 会从 ZooKeeper 同步元数据,但还不接管写入。配置里需要加zookeeper.connect和controller.quorum.voters两组参数。
第三阶段,把 Broker 逐个切换到 KRaft 模式。每个 Broker 重启时改配置,去掉zookeeper.connect,加上controller.quorum.voters。重启后 Broker 会从 Controller 拉取元数据,不再连 ZooKeeper。
第四阶段,所有 Broker 切换完成后,Controller 停止从 ZooKeeper 同步,正式接管元数据管理。这时候 ZooKeeper 就可以下线了。
注意:迁移过程中集群会经历多次滚动重启,每次重启都会影响一部分分区的可用性。建议在业务低峰期操作,并且确保 Topic 的副本因子至少为 3,这样单 Broker 重启不会导致分区不可用。
我踩过的一个坑是:迁移过程中如果 Controller 和 ZooKeeper 的元数据不一致,会导致部分 Topic 的分区 Leader 信息错乱。排查方法是对比 Controller 日志里的元数据版本号和 ZooKeeper 里的 znode 版本号。如果对不上,需要暂停迁移,手动触发一次全量同步。
4. 常见问题与排查技巧实录
4.1 Controller 选举卡住或反复失败
这是 KRaft 集群最常见的问题。现象是 Controller 日志里反复出现Starting leader election但没有Leader election completed,或者选举完成后很快又发起新选举。
排查思路分三步。第一步,检查controller.quorum.voters配置是否三个节点完全一致。任何一个节点配错,都会导致它无法参与投票,多数派凑不齐。第二步,检查网络连通性。Controller 之间用的是 9093 端口,用telnet或nc测试一下端口是否可达。第三步,检查node.id是否重复。如果两个节点用了同一个 ID,Raft 会认为它们是同一个节点,导致投票混乱。
我遇到过一次选举卡住,最后发现是某个 Controller 节点的磁盘满了,写不进日志,自然无法参与投票。所以磁盘监控一定要做好,元数据日志目录至少留 20% 的余量。
4.2 Broker 无法注册到 Controller
Broker 启动后日志里一直报Connection to controller failed或者Registration failed。这种情况通常是 Broker 的controller.quorum.voters配错了,或者 Controller 的监听地址 Broker 访问不到。
检查方法:在 Broker 节点上用nc -zv ctrl1 9093测试连通性。如果端口不通,检查 Controller 的listeners配置和防火墙规则。如果端口通但注册失败,检查 Broker 的node.id是否和已有节点冲突。
还有一个隐蔽的原因:Controller 的controller.listener.names和 Broker 的listeners协议不匹配。比如 Controller 配了CONTROLLER://,Broker 却用PLAINTEXT://去连,协议对不上就会失败。确保两边的监听器名称和协议一致。
4.3 元数据操作变慢的排查
如果创建 Topic、修改分区这类操作明显变慢,可以从几个方向排查。先看 Controller 的日志,有没有Snapshot generation相关的耗时记录。快照生成太频繁会拖慢元数据写入,可以调大metadata.log.max.record.bytes.between.snapshots。
再看 Controller 节点的磁盘 IO。元数据日志是顺序追加,对磁盘顺序写性能要求高。如果用的是机械盘或者共享存储,IO 延迟可能成为瓶颈。建议 Controller 节点用本地 SSD。
还有一个容易忽略的点:Broker 数量太多。每个 Broker 都要从 Controller 拉取元数据,Broker 越多,Controller 的元数据分发压力越大。如果集群有几百个 Broker,可以考虑增加 Controller 节点数,把元数据分发压力分散开。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方式 |
|---|---|---|---|
| Controller 选举卡住 | voters 配置不一致 | 对比三个节点配置 | 统一配置后重启 |
| Broker 注册失败 | 网络不通或端口错误 | nc 测试端口 | 检查防火墙和监听配置 |
| 元数据操作慢 | 快照频繁或磁盘 IO 高 | 查看 Controller 日志和磁盘监控 | 调大快照间隔,换 SSD |
| 集群启动后不可用 | Controller 数量不足多数派 | 检查启动顺序和节点状态 | 确保多数 Controller 在线 |
| 迁移后 Topic 丢失 | 元数据同步不完整 | 对比 ZooKeeper 和 Controller 元数据 | 回滚迁移,重新同步 |
4.5 几个我踩过的坑和独家技巧
第一个坑:格式化存储目录时用错集群 ID。kafka-storage.sh format的-t参数如果和实际集群 ID 不一致,节点启动后会拒绝加入集群。这个错误在日志里不明显,只报Inconsistent cluster ID。解决办法是重新格式化,但注意格式化会清空元数据,生产环境操作前一定要备份。
第二个坑:Controller 和 Broker 混部时资源竞争。测试环境图省事把两种角色放一起,结果消息吞吐一上来,Controller 的元数据写入就被拖慢,导致 Broker 心跳超时,触发不必要的重平衡。生产环境一定要分离部署,至少把元数据日志目录和消息数据目录放在不同磁盘上。
第三个技巧:用kafka-metadata-quorum.sh工具查看元数据状态。这个工具可以查看当前 Leader、Follower 列表、日志偏移量等信息,排查问题时非常有用:
bin/kafka-metadata-quorum.sh --bootstrap-server localhost:9092 describe --status输出里会显示CurrentLeader、CurrentVoters、MaxFollowerLag等关键指标。如果MaxFollowerLag持续增大,说明某个 Follower 同步跟不上,需要检查它的网络或磁盘。
第四个技巧:监控元数据日志的磁盘使用量。KRaft 的元数据日志虽然不大,但快照文件会累积。如果快照清理不及时,磁盘可能被撑满。建议配置监控,当metadata.log.dir使用率超过 70% 时告警。
5. 性能调优与规模上限的实测观察
5.1 分区数量对元数据的影响
KRaft 相比 ZooKeeper 最大的优势之一就是分区规模上限提高。我在测试环境做过对比:同样配置下,ZooKeeper 模式在分区数到 15 万左右时,创建 Topic 的延迟从几百毫秒涨到十几秒;KRaft 模式在分区数到 50 万时,创建 Topic 延迟还在 1 秒以内。
但这个提升不是无条件的。分区数增加会带来元数据日志膨胀,快照文件变大,Controller 内存占用增加。我实测下来,每个分区的元数据大约占 1KB 到 2KB 内存,50 万分区大概需要 1GB 左右的 Controller 堆内存。所以 Controller 节点的 JVM 堆建议至少 4GB,给元数据留足空间。
5.2 Controller 节点数量的取舍
3 个 Controller 能容忍 1 个故障,5 个能容忍 2 个。但 Controller 越多,Raft 的写入延迟越高,因为 Leader 要等更多 Follower 确认。我实测过 3 个和 5 个 Controller 的写入延迟差异:3 个时元数据写入 P99 在 15ms 左右,5 个时涨到 25ms 左右。
所以选择 Controller 数量要权衡可用性和延迟。大部分场景 3 个足够,除非你的集群规模特别大,或者对可用性要求极高,才考虑 5 个。超过 5 个没有意义,Raft 的多数派机制决定了 7 个节点的容错能力只比 5 个多 1 个,但延迟增加明显。
5.3 元数据日志的磁盘选型
Controller 节点的磁盘性能直接影响元数据写入延迟。元数据日志是顺序追加,对顺序写性能要求高,对随机读要求低。所以选盘的时候优先看顺序写 IOPS 和延迟。
我用过三种盘做对比:机械盘顺序写延迟在 5ms 到 10ms,SATA SSD 在 1ms 到 2ms,NVMe SSD 在 0.1ms 到 0.5ms。元数据写入延迟直接受磁盘影响,所以 Controller 节点强烈建议用 NVMe SSD。如果预算有限,至少用 SATA SSD,不要用机械盘。
提示:元数据日志目录和消息数据目录分开放在不同磁盘上,可以避免消息写入的 IO 压力影响元数据写入。这个在 Broker 和 Controller 混部的场景下尤其重要。
6. 写在最后的一些个人体会
KRaft 这套东西刚出来的时候,我也觉得"不就是把 ZooKeeper 换掉吗",但真正用起来才发现,它改变的不只是部署方式,而是整个集群的运维逻辑。以前排查问题要同时看 Kafka 和 ZooKeeper 两套日志,现在只看 Kafka 自己的元数据日志就够了。以前 Controller 切换要等几十秒,现在几秒就完成。这些变化在平时可能感觉不明显,但在故障时刻,每一秒的恢复时间都意味着真金白银。
我个人的建议是:如果你还在用 ZooKeeper 模式,可以开始规划迁移了。不用急着上生产,先在测试环境跑通流程,把配置和排查方法摸熟。迁移过程中最需要注意的是元数据一致性,每次滚动重启后都要确认 Controller 和 Broker 的元数据版本对得上。另外,Controller 节点的磁盘和网络一定要给足资源,这是整个集群的"大脑",大脑卡了,全身都动不了。
最后分享一个小技巧:KRaft 的元数据日志可以用kafka-dump-log.sh工具直接查看内容,排查元数据问题时非常有用。命令是bin/kafka-dump-log.sh --cluster-metadata-decoder --files /data/kraft-metadata/00000000000000000000.log,输出里能看到每条元数据记录的详细内容,包括操作类型、操作对象、时间戳等。这个工具在排查"某个 Topic 为什么不见了"这类问题时特别管用。