接上一篇把 KRaft 基础部署跑通之后,这篇专门聊聊生产环境里真正要面对的硬骨头。KRaft 模式上线不难,难的是架构怎么规划、ZooKeeper 怎么迁、指标怎么盯、出了问题怎么救。这篇的内容都是我实际部署和运维 Kafka KRaft 集群时反复踩过又填平的坑,按生产环境的真实顺序来讲,适合已经能跑通单机或测试集群、准备把手上的 ZooKeeper 集群往 KRaft 迁的同学参考。
1. 生产架构规划:控制器节点到底该怎么放
1.1 角色合租还是分居,取决于你的集群体量
KRaft 模式把原来的 ZooKeeper 职责收编成了 controller 角色,部署时每个节点可以只当 controller、只当 broker,也可以两个角色合并。这个选择没有绝对标准,但实际跑下来有一条很清晰的分界线:节点总数在 10 个以内、分区数不超过 2000 的集群,合租模式完全够用;再往上走,强烈建议把 controller 独立出来。
我见过不少团队一上来就按大厂方案拆三套 controller,结果小集群白白多养了三台机器,资源利用率惨不忍睹。反过来,也有集群到了几千个分区还让 controller 和 broker 挤在一起,metadata 请求把 CPU 打满,broker 的读写延迟跟着遭殃。所以先别抄作业,算清楚自己的分区规模和节点数量再定。
合租模式最大的好处是省机器、部署简单,三个节点就能同时承担 controller 和 broker 职责。但代价是 controller 的负载和 broker 的数据读写互相影响,遇到 metadata 密集型场景(比如大量 topic 频繁创建删除、分区重新分配)时,broker 侧的请求延迟会明显波动。独立 controller 模式则相反,牺牲三台机器换来了职责隔离,controller 可以放心加大内存、调高 GC 参数,broker 侧的抖动也小很多。
1.2 仲裁节点的数量和网络分区容忍度
KRaft 的 controller 用的是 Raft 协议,仲裁节点数量必须是奇数,生产最小规模是 3 个。这里的“3”不是拍脑袋定的,它决定了集群在故障时的可用性边界:3 个仲裁节点允许挂 1 个,5 个允许挂 2 个。你可能会问,那直接用 5 个是不是更稳?也不尽然,仲裁节点越多,Raft 提交一条元数据变更需要达成的多数派就越大,跨机房场景下延迟会更明显。
这里有一个很多教程不会提醒你的点:仲裁节点的网络拓扑直接影响整个集群的可用性。Raft 协议天然容忍网络分区,但前提是多数派能凑齐。如果你把 3 个仲裁节点放在同一个机架,机架整体断电时集群就完全不可用了;如果分散在 3 个可用区,那么任意一个可用区挂掉,剩下两个可用区里的仲裁节点依然能凑成多数派,集群继续对外服务。所以做跨机房或跨可用区部署时,仲裁节点一定要打散放置,这是 KRaft 架构里最容易被低估的一步。
另外,controller 和 broker 的节点角色是启动参数决定的,一个节点只能绑定一个node.id,不能既在这个集群当 controller 又去别的集群当 broker。如果你想复用机器,可以用虚拟机或容器隔离开,但生产上不推荐这么干,隔离性太差。
2. KRaft 配置文件的几个关键参数:照着调不会出事
2.1 必须改的配置项和容易踩的坑
KRaft 模式的config/server.properties和 ZooKeeper 时代相比,删掉了一整套zookeeper.connect相关配置,换成了一组新的 controller 参数。我按生产可用的模板把重点列一下:
| 参数 | 推荐值 | 说明 |
|---|---|---|
process.roles | broker,controller或controller | 决定节点角色,合租模式写broker,controller |
node.id | 每个节点唯一 | 原来叫broker.id,KRaft 里统一叫node.id |
controller.quorum.voters | 三个节点标识 | 格式为id@host:port,多个用逗号分隔 |
controller.listener.names | CONTROLLER | 自定义监听器名,必须在listener.security.protocol.map里声明 |
listeners | PLAINTEXT://:9092 | broker 对外监听,按需加 SASL_SSL |
advertised.listeners | 外部可访问地址 | 千万不能漏,漏了外部客户端连不上 |
log.dirs | 建议独立数据盘 | 不要和系统盘混用,后面详聊 |
metadata.log.dir | controller 专属目录 | 当前还在log.dirs范围内,但建议为它单独规划 |
这里单独讲一下 controller 的两个监听器配置。很多新手会被controller.listener.names=CONTROLLER和listener.security.protocol.map=CONTROLLER:PLAINTEXT绕晕。原理其实很简单:controller 之间的通信走的是独立的监听器,不经过 broker 的对外端口。你只要记住,listener.security.protocol.map里必须把CONTROLLER这个键声明出来,否则启动直接报错。如果走加密内网,协议可以设成SSL或SASL_SSL,但要注意证书覆盖到所有仲裁节点。
还有一个高频错误是controller.quorum.voters里写的 host 必须能被所有 broker 解析。如果 broker 节点和 controller 节点不在同一网段,这里要写内网 IP 或者 hosts 里能查到的域名,不要写localhost。我遇到过同事图省事全写 localhost,单机测试没事,一上多节点就疯狂报连接拒绝,排查半天全是这个低级错误。
2.2 存储目录的规划:分区就是把事故隔离
KRaft 模式里,broker 的数据目录依然由log.dirs管理,可以配多个目录,Kafka 会自动做分区级负载均衡。但 controller 节点的元数据目录metadata.log.dir是 Raft 日志和快照的存放地,它的事务敏感度远高于普通数据目录。我的建议是:用单独的磁盘放 metadata,别和数据盘、系统盘混在一起。磁盘 IO 打满时,Raft 日志写入延迟升高会直接拖慢整个集群的元数据操作,甚至会触发控制器切换,这种事故排查起来相当痛苦。
另外,log.dirs多目录场景下,单块盘故障只会影响放在它上面的分区,其他盘上的分区不受影响。这等于天然做了一层故障隔离。对于 broker 节点,我习惯用 2 到 3 块数据盘做log.dirs,而不是把所有分区压在一块盘上。数据盘选择上,普通 SAS 盘或云盘即可,吞吐优先的场景再考虑本地 NVMe 盘。
2.3 格式化流程:比 ZooKeeper 时代简单但别手滑
KRaft 模式不再需要zookeeper-server-start.sh来初始化和格式化 ZooKeeper,取而代之的是kafka-storage.sh工具。整套初始化流程只有两步:先用kafka-storage.sh random-uuid生成一个集群 UUID,再用kafka-storage.sh format -t <UUID> -c <config>格式化每个节点。
一个必须注意的坑:format 命令会把log.dirs和metadata.log.dir里已有的数据清掉,执行前务必确认该目录没有正在使用的数据。特别是做演练或复盘时,如果节点之前跑过别的集群,format 会把人家数据全抹了。我一般在 format 前先ls看一眼目录内容,确认是空的再动手。格式化完成后,config/server.properties里的 UUID 不要随便改,改了之后节点之间就认不出彼此了,启动报错会让你一脸懵。集群 UUID 可以事后通过kafka-storage.sh info -c <config>查询,如果忘了也没关系。
3. ZooKeeper 集群平滑迁移到 KRaft 的完整路径
3.1 迁移前的条件盘点:版本和配置双检查
从 ZooKeeper 模式迁移到 KRaft,Kafka 官方支持从 3.3 及以上版本原地滚动升级。但在正式动刀之前,我对每个集群都会做一遍强制体检,少一项都别继续:
- Kafka 版本必须 >= 3.3,如果是老版本,先升级到 3.6 或 3.7 再迁移,一步到位不折腾;
- 确认所有配置项里没有
zookeeper.connect残留,KRaft 启动时遇到这个参数会直接拒绝启动; - 清楚记录当前集群的
log.dirs、副本因子、分区数,迁移前后要做数据对比; - 预留足够的磁盘空间,迁移过程会生成一份新的元数据目录,空间不足可能中途失败;
- 迁移期间禁止执行 topic 增删、分区扩容等元数据变更操作,否则可能造成元数据不一致。
这一步最反直觉的是:迁移过程中集群还跑在 ZooKeeper 模式上,但 broker 的配置里已经要去掉 ZooKeeper 参数了。官方设计的迁移窗口期(也就是"双模式"阶段)里,broker 节点会同时维持与 ZooKeeper 的旧通信和控制器的 Raft 通信,配置非常容易被搞混。我建议迁移前把配置差异整理成一个 diff 清单,逐项核对。
| 配置项 | ZooKeeper 模式 | KRaft 迁移模式 |
|---|---|---|
zookeeper.connect | 必须配置 | 必须移除 |
process.roles | 不配置 | 必须配置 |
controller.quorum.voters | 不配置 | 必须配置 |
broker.id | 可配置 | 改名为node.id |
listener.security.protocol.map | 无控制器监听器 | 必须包含CONTROLLER |
3.2 分阶段操作:先起控制器,再逐步切 broker
官方迁移分成两个大阶段。第一阶段先把 controller 节点拉起来,让它们形成 Raft 仲裁并接管元数据;第二阶段把 broker 逐个切换配置,让它们从依赖 ZooKeeper 过渡到依赖控制器。实际操作时顺序是:
- 准备 3 台 controller 节点(或合租节点的 controller 角色配置),执行
kafka-storage.sh format生成 KRaft 元数据; - 启动 controller 节点,确认它们通过
controller.quorum.voters互相看到、选举出 leader; - 逐个升级 broker:停掉 broker → 修改
server.properties中 ZooKeeper 相关配置 → 重新格式化 broker 的数据目录 → 启动 broker; - 确认所有 broker 都注册到 KRaft 控制器后,执行指令将集群标记为“已迁移”状态;
- 观察一段时间,确认元数据同步正常后,关闭所有 ZooKeeper 节点。
有一个细节很多人会忽视:迁移完成后,ZooKeeper 里的旧元数据不会自动清理。如果你之后还要回滚,这些数据就是救命稻草;如果确定不再回滚,记得手动清理相关 ZNode,避免数据残留干扰将来的排障。但清理时机务必放在迁移稳定运行至少两周后,别刚切完就急着删,给自己留条后路。
3.3 迁移后的验证清单
迁移不是"能启动就算成功",我每次迁移完都会跑一遍下面的验证:
- 用
kafka-metadata-quorum.sh查看控制器状态,确认所有 broker 都在线、控制器有 leader; - 用
kafka-topics.sh --describe查一遍核心 topic 的副本分布,和迁移前的记录做比对; - 用生产流量抽样跑一轮生产和消费验证,重点看有没有报
Metadata相关的超时异常; - 观察 broker 日志里是否还有 ZooKeeper 相关的告警或错误。
此处有一条很实用的经验:没有对比就没有发言权。迁移前我建议在同一个业务 topic 上记录一条消息的生产消费往返延迟,迁移后跑同样的操作做对比。KRaft 模式在元数据操作频繁的场景下,延迟通常比 ZooKeeper 模式低,因为省掉了一次 ZooKeeper 写入的往返。
4. KRaft 集群的监控指标与可视化工具:盯住这四类
4.1 四类必盯指标:控制器状态、仲裁延迟、元数据加载、磁盘
KRaft 集群的监控指标和 ZooKeeper 时代差别很大,沿用老监控模板会漏掉一堆关键信号。我实际部署时重点盯四类指标。
第一类是控制器状态。核心指标是ControllerStats相关的ActiveControllerCount,正常情况下整个集群应该恰好为 1。如果出现 0 或者大于 1,说明控制器选举出了问题,这是最高优先级的告警。另外要盯ControllerQuorum相关的指标,比如当前 leader 是否稳定,是否有频繁切换。控制器频繁切换通常意味着网络抖动或者仲裁节点间时钟偏差过大,需要立刻处理。
第二类是仲裁通信延迟。Raft 仲裁节点之间的请求延迟是元数据操作的上限,这个值一旦持续走高,topic 创建、分区扩容都会变慢。JMX 里可以通过kafka.controller:type=KafkaController,name=RaftRequestLatency这类指标观察。实际经验是,仲裁通信延迟的正常基线应该在毫秒级,超过 100ms 就要开始查网络了。
第三类是broker 侧元数据加载情况。KRaft 模式下 broker 启动时要向控制器拉取全量元数据,这个加载时间取决于元数据量和网络带宽。可以通过 broker 启动日志中的load metadata耗时来评估,正常情况下应该秒级完成。如果加载时间过长,检查控制器快照大小和网络带宽。
第四类是磁盘和日志目录。除了常规磁盘使用率,还要盯kafka.log:type=Log下的日志分段数量、flush 延迟。KRaft 模式下 controller 的元数据日志目录如果持续增长且快照不及时生成,会导致 broker 启动时拉取元数据时间越来越长,这是一个容易被忽略的隐性风险。
4.2 可视化工具选型:Kafka UI(AKHQ)与 CMAK 的取舍
监控指标收集上来之后,还得有趁手的可视化工具。很多团队还在用老牌的 CMAK(原 Kafka Manager),它虽然能管理 topic、查看消费组,但有一个硬伤:CMAK 默认强依赖 ZooKeeper,对 KRaft 模式的支持非常有限。我建议 KRaft 集群优先用 AKHQ(也叫 Kafka UI)或其他原生适配 KRaft 的工具。
AKHQ 的部署很简单,一个 Docker 容器或 JAR 包就能跑起来,配置里直接写 broker 的 bootstrap 地址和 Schema Registry 地址即可。实际体验下来,AKHQ 对 KRaft 的支持比较到位,能看到控制器状态、分区副本分布、消费组 lag,甚至直接在 UI 上查看 Kafka Connect 的任务状态。它同时支持多集群管理,如果你的环境里既有 ZooKeeper 集群又有 KRaft 集群,可以在同一个 AKHQ 实例里统一纳管,切换时不用频繁换工具,运维效率提升很明显。
选型时有两个实用建议:一是不要迷信所谓“功能最全”的工具,KRaft 迁移初期你需要的核心能力是查看控制器状态和消费组 lag,这两点 AKHQ 都做得够用;二是务必用官方支持列表确认工具与 Kafka 版本的兼容性,有些 UI 工具只支持旧协议,连上 KRaft 集群后连 topic 列表都拉不出来,白白浪费时间。
4.3 告警阈值参考:给你一份能直接用的配置
有了指标就要配上告警,我把自己的告警阈值整理成下表,生产环境可以直接抄:
| 告警项 | 阈值 | 说明 |
|---|---|---|
| ActiveControllerCount != 1 | 持续 1 分钟 | 控制器异常,最高优先级 |
| 控制器切换频率 | 5 分钟内超过 3 次 | 可能网络抖动或仲裁节点故障 |
| KafkaController 进程存活 | 0 | 宕机即告警 |
| 仲裁通信延迟 | 超过 100ms 持续 5 分钟 | 影响全部元数据操作 |
| 磁盘使用率 | 超过 80% | 提前规划扩容或清理日志 |
| broker 请求处理延迟 | 超过基线 2 倍持续 10 分钟 | 需要排查热点分区或 GC 问题 |
| 消费组 lag | 超过阈值 10 分钟 | 业务消费积压预警 |
5. KRaft 模式运维复盘:我踩过的坑和恢复技巧
5.1 六个高频问题速查表
这一节的内容都是我在实际运维中遇到过的真实问题,直接对照排查即可。我把它们整理成了速查表,按发生频率排序:
| 现象 | 根因 | 处理办法 |
|---|---|---|
启动报错Invalid zookeeper.connect | 配置里残留 ZooKeeper 参数 | 删除该配置,确认只使用 KRaft 参数 |
| 控制器一直选不出 leader | 仲裁节点数量不足或网络不通 | 检查controller.quorum.voters的地址与连通性 |
| broker 启动时元数据加载超时 | 控制器快照太大或带宽不足 | 调整metadata.max.snapshot.interval.ms,或扩容网络 |
| 集群 UUID 不一致 | format 时用了不同的 UUID | 改用同一个 UUID 重新格式化数据目录 |
| 磁盘 IO 高导致元数据延迟 | 数据盘和元数据盘混用 | 为metadata.log.dir单独分配磁盘 |
| 分区副本长期处于 offline | 磁盘故障或容量满 | 检查磁盘状态,必要时用kafka-reassign-partitions.sh迁移副本 |
5.2 一条救命经验:快照与恢复的时机选择
KRaft 模式里,controller 会定期把元数据做成快照(Snapshot),快照的生成频率由metadata.max.snapshot.interval.ms控制。这个参数太大会导致快照过大、broker 拉取慢;太小又会让快照生成频繁、写放大明显。生产环境我一般设置为 12 到 24 小时,同时保留最近 2 到 3 个快照备份。磁盘空间足够时可以适度调高保留数量,方便回滚到更多历史时间点。
恢复操作的教训我记忆犹新:一次演练中我误操作删除了一台 broker 的元数据目录,想着用另一台节点的快照恢复,结果因为快照不在同一时间点,恢复后集群里出现了一批缺失的 metadata 记录。恢复的底线原则是:快照和日志要成对保留,不能只留快照不看日志,否则元数据一定不完整。Raft 日志在正常运转时不会被清理,恢复时必须从最后一个快照之后的所有日志段一起重放,少一段都不行。如果你不确定自己的恢复操作是否完整,宁可重新 format 再重新加入集群,也不要硬着头皮启动一个元数据不全的 controller。
5.3 滚动重启的正确姿势
日常运维免不了要重启节点升级版本或调整参数,KRaft 集群的滚动重启跟 ZooKeeper 时代不一样,需要注意顺序。我推荐的顺序是:先重启 broker,后重启 controller。原因是 broker 重启后需要向 controller 拉取元数据,如果先把 controller 全停了,broker 会陷入元数据拉取超时的状态,恢复时间会显著拉长。
逐个重启时,要等一个节点的所有分区完成 leader 转移后再操作下一个节点。可以用kafka-leader-election.sh主动触发优雅的 leader 转移,减少分区不可用时间。对 controller 节点的重启要格外谨慎,一次只能重启一个,等它重新加入仲裁并且确认集群仍有一个稳定 leader 后,再重启下一个。如果你同时停掉两个仲裁节点,在 3 节点仲裁里就凑不齐多数派,整个集群的元数据操作会全部阻塞。
6. 迁移后的性能调优和长期维护心得
6.1 几个值得调的 KRaft 专属参数
KRaft 模式有几个参数是 ZooKeeper 时代没有的,在性能调优时值得逐个过一遍:
controller.quorum.request.timeout.ms和controller.quorum.retry.backoff.ms控制仲裁请求的超时与重试,网络抖动频繁的环境可以适度调大,避免误判故障。metadata.log.dir的独立规划属于老生常谈,但我要再强调一次:生产故障里,controller 节点的元数据盘 IO 被打满导致整个集群降低可用性的案例比你想的多得多。
num.io.threads和num.network.threads这两个参数在 broker 高吞吐场景下的调优,本质上和 ZooKeeper 时代没有区别,但是 KRaft 模式下 broker 还要承担向 controller 同步元数据的开销,所以线程数的设置可以比原来保守一些,留出余量。实测下来,100 分区以内的集群用默认值就够,分区上千之后才需要逐个压测调整。
6.2 多环境一致性:用配置模板管理集群
KRaft 部署的一个隐性成本是配置文件变多、角色多样,很容易出现“测试环境好好的,生产环境启动报错”的尴尬。我现在的做法是用一套配置模板管理所有集群,通过环境变量填充节点角色、监听地址和仲裁地址。这样不管测试、预发还是生产,逻辑配置完全一致,差异全部收敛在变量里。效果很直观:手误的概率低了,新环境上线的速度也快了。
配置模板里我会强制加入几组注释,把每个参数的含义、推荐值、修改代价写清楚。新人接手的时候不需要翻官方文档就能看懂,也减少了拿着生产配置瞎试的风险。这一招看起来简单,实际运维里节省的时间非常可观。
6.3 版本升级的节奏建议
KRaft 模式从 Kafka 3.3 开始引入,到 3.6 之后已经比较成熟。如果你所在的公司还在用 3.3 或 3.4,我建议至少升到 3.6 以上再大规模上生产。我的理由有三个:一是 3.6 对 KRaft 的元数据兼容性和稳定性做了大量修复;二是 3.6 之后的官方文档和社区资料更多,踩坑时有处可查;三是新版本对 KRaft 的监控指标补全了不少,对运维友好很多。
升级时依然用滚动重启,不要跨大版本跳级。比如从 3.6 升到 3.7 没问题,但从 3.3 直接跳 3.8 风险就不可控了。每次升级后留出至少一个观察窗口,确认指标平稳后再升下一版。总的来说,在 KRaft 迁移这件事上,"慢就是快"——配置检查做细一点、迁移窗口拉长一点、回滚方案备好一点,后面省下的是整夜整夜救火的精力。
我个人在实际操作中养成的习惯是:每次迁移或大版本升级前,把集群当前的配置、分区分布、核心指标基线导出存档;操作完成后用一套写好的脚本自动比对前后差异。这套流程看着不复杂,但能逼着你把每个操作步骤想清楚,也让你在出问题时总有据可查。如果你的团队正准备做 KRaft 迁移,希望这篇的经验能帮你少走一段弯路。