升级出问题,通常不是「版本不兼容」四个字能解释的
线上做 Kafka 升级,事后复盘最常见的结论是「客户端版本太低」。这句话往往掩盖了真正的原因:可能是客户端用了新协议而服务端不认,可能是某个工具类的包名在 4.x 挪了位置,也可能是新 broker 对配置校验收紧了,旧配置文件原样搬过去起不来。
这三种情况的表现完全不同,处理方式也不同。所以这一节不急着给升级步骤,先把「版本」这个词拆开。
同时存在三个版本号
很多人以为 Kafka 只有一个版本:4.3.0。实际上升级过程中要管住三个互相独立的东西。
| 名称 | 取值示例 | 谁决定 | 升级时怎么变 |
|---|---|---|---|
| broker/客户端二进制版本 | 4.3.0 | 你下载或拉取的包 | 换 jar 或换镜像 |
metadata.version | 4.3-IV0 | 集群元数据格式 | 换完二进制后单独执行 |
inter.broker.protocol.version | KRaft 下由 metadata 版本推导 | 集群 | 随之变化 |
第二个是最容易被忽略的。它描述的不是「broker 是哪个版本」,而是「集群的元数据写成了哪种格式」。换完二进制,metadata.version 不会自动升——这是一个需要显式操作的决定。
本机在 4.3.0 单节点上执行:
docker exec mq-f1 /opt/kafka/bin/kafka-features.sh \
--bootstrap-server 127.0.0.1:9092 describe
原始输出(本机实测,Temurin 25.0.5+8 / Kafka 4.3.0 KRaft 单节点 / 2026-10-29):
Feature: eligible.leader.replicas.version SupportedMinVersion: 0 SupportedMaxVersion: 1 FinalizedVersionLevel: 1 Epoch: 1576
Feature: group.version SupportedMinVersion: 0 SupportedMaxVersion: 1 FinalizedVersionLevel: 1 Epoch: 1576
Feature: kraft.version SupportedMinVersion: 0 SupportedMaxVersion: 1 FinalizedVersionLevel: 0 Epoch: 1576
Feature: metadata.version SupportedMinVersion: 3.3-IV3 SupportedMaxVersion: 4.3-IV0 FinalizedVersionLevel: 4.3-IV0 Epoch: 1576
Feature: share.version SupportedMinVersion: 0 SupportedMaxVersion: 1 FinalizedVersionLevel: 1 Epoch: 1576
Feature: streams.version SupportedMinVersion: 0 SupportedMaxVersion: 1 FinalizedVersionLevel: 1 Epoch: 1576
metadata.version 那一行信息量最大:
SupportedMinVersion: 3.3-IV3—— 这个 4.3.0 的 broker 仍然能读 3.3 时代的元数据格式。这是向下兼容的下界。SupportedMaxVersion: 4.3-IV0—— 它能写的最新格式。FinalizedVersionLevel: 4.3-IV0—— 当前集群实际在用的格式。
三者相等时说明这个集群已经「升到位」了。如果 FinalizedVersionLevel 停在 3.9-IV0,说明二进制换了但元数据格式没升——这是一个合法的中间状态,也是滚动升级过程中必然经过的状态。
对照 3.9.0 的 broker,日志里格式化的那一行是:
Formatting metadata directory /tmp/kraft-logs with metadata.version 3.9-IV0.
3.9-IV0 落在 4.3.0 的 [3.3-IV3, 4.3-IV0] 区间内,所以 4.3.0 的 broker 可以直接加入 3.9.0 的集群。反过来则不成立:3.9.0 的 broker 不认识 4.x-IV0,它自己的上界就是 3.9-IV0。
客户端和服务端是两条升级线
kafka-clients 和服务端同号发布,但可以分开升级。一个 4.3.0 的 broker 集群里跑着 3.9.0 的客户端,是完全正常的部署状态,不是「凑合着用」。
官方给出的兼容承诺是:客户端与服务端在大版本 N 和 N-1 之间双向兼容。实际能不能连、连上之后哪些特性可用,要靠实测。本机做了四组交叉。
测试用一个只做三件事的探针:打印客户端版本、发一条消息拿 offset、用 AdminClient 读 cluster id 和节点地址。
public class VersionProbe {
public static void main(String[] a) throws Exception {
String bs = a[0], topic = a[1];
System.out.println("client=" + AppInfoParser.getVersion());
Properties p = new Properties();
p.put("bootstrap.servers", bs);
p.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
p.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
try (KafkaProducer<String, String> pr = new KafkaProducer<>(p)) {
RecordMetadata md = pr.send(new ProducerRecord<>(topic, "k", "v")).get(20, TimeUnit.SECONDS);
System.out.println("send OK partition=" + md.partition() + " offset=" + md.offset());
}
}
}
编译一次,运行时换 classpath 里的 kafka-clients jar。四组结果:
| 组 | 客户端 | 服务端 | 结果 |
|---|---|---|---|
| A | 4.3.0 | 4.3.0 | 成功 |
| B | 3.9.0 | 4.3.0 | 成功 |
| C | 2.5.1 | 4.3.0 | 成功 |
| D | 4.3.0 | 3.9.0 | 成功 |
原始输出(本机实测,2026-10-29):
### A: 4.3.0 客户端 -> 4.3.0 broker
client=4.3.0
send OK partition=0 offset=0
clusterId=5L6g3nShT-eMCtK--X86sw
node=1 127.0.0.1:19251
### B: 3.9.0 客户端 -> 4.3.0 broker
client=3.9.0
send OK partition=0 offset=1
### C: 2.5.1 客户端 -> 4.3.0 broker
client=2.5.1
send OK partition=0 offset=2
### D: 4.3.0 客户端 -> 3.9.0 broker
client=4.3.0
send OK partition=0 offset=0
node=1 127.0.0.1:19261
C 组值得单独说一句。2.5.1 是 2020 年的客户端,跨了三个大版本,官方兼容承诺并不覆盖它。它「能连能发」是事实,但不能据此认为它被支持:老客户端不知道新 broker 的新错误码,遇到需要新语义的场景(比如分区迁移中的某些状态)会静默走错分支。生产上把这种客户端留着不动,是一笔迟早要还的债。
什么叫硬下界:新协议在旧 broker 上不可能工作
上面四组都是「发一条消息」,这个操作几乎用不到任何新协议。真正的兼容边界要拿新特性去撞。
KIP-848 引入的 group.protocol=consumer 是 4.0 之后才有的消费组协议。用 4.3.0 的客户端分别连两个版本的 broker:
Properties p = new Properties();
p.put("bootstrap.servers", bs);
p.put("group.id", "gv-" + proto);
p.put("group.protocol", proto); // consumer 或 classic
p.put("auto.offset.reset", "earliest");
本机实测结果:
4.3broker consumer -> proto=consumer OK records=3
4.3broker classic -> proto=classic OK records=3
3.9broker consumer -> proto=consumer FAILED UnsupportedVersionException:
The cluster does not support the new CONSUMER group protocol.
Set group.protocol=classic on the consumer configs to revert to the CLASSIC protocol ...
3.9broker classic -> proto=classic OK records=1
这才是「硬下界」的样子:不是降级处理,不是警告,是直接抛 UnsupportedVersionException,并且报错信息里直接告诉你退路——把客户端配置改回 classic。
所以「客户端 4.3.0 连 3.9.0 broker 能用吗」这个问题没有统一答案。用默认配置(group.protocol=classic)能;显式开了新协议的不能。升级顺序因此有了约束:先升 broker,再开客户端的 group.protocol=consumer,反过来一定失败。
反向的坑:新 broker 对配置只严不松
升级时另一个常被忽略的方向是配置文件。旧版本能启动的配置,新版本未必接受。
本机在同一个配置模板上起 3.9.0 时遇到了这个:
Exception in thread "main" java.lang.IllegalArgumentException: requirement failed:
advertised.listeners cannot use the nonroutable meta-address 0.0.0.0.
Use a routable IP address.
触发它的配置并不离谱:
listeners=PLAINTEXT://0.0.0.0:19092,CONTROLLER://0.0.0.0:9093
advertised.listeners=PLAINTEXT://127.0.0.1:19261
问题在于 advertised.listeners 只写了 PLAINTEXT,没写 CONTROLLER。缺失的那个 listener 会从 listeners 继承 0.0.0.0,而 3.9.0 的校验发现 advertised 里出现了 0.0.0.0 就直接拒绝启动。
补全之后(本机实测,2026-10-29):
listeners=PLAINTEXT://0.0.0.0:19092,CONTROLLER://127.0.0.1:9093
advertised.listeners=PLAINTEXT://127.0.0.1:19261,CONTROLLER://127.0.0.1:9093
才起得来。这个坑的教训不是「记住要写全」,而是:升级前要把每一条 listeners 和 advertised.listeners 的关系看一遍,尤其是多 listener 的部署。
工具类的包名也变了
书里反复用 JMX 取指标。旧资料写的类名是:
kafka.tools.JmxTool
在 4.3.0 的镜像里这样调会直接报找不到类:
Error: Could not find or load main class kafka.tools.JmxTool
正确的位置在 kafka-tools-4.3.0.jar 里,包名已经改成 org.apache.kafka.tools:
java -cp "/opt/kafka/libs/*" org.apache.kafka.tools.JmxTool \
--jmx-url service:jmx:rmi:///jndi/rmi://127.0.0.1:9999/jmxrmi \
--object-name "kafka.server:type=BrokerTopicMetrics,name=MessagesInPerSec" \
--attributes Count --one-time true
凡是脚本里写死了 kafka.tools.* 的,升级后都要改。 这类问题不会在启动日志里报警,只会在你要用工具的那一刻才暴露——通常是最不该出问题的时候。
KRaft 下的滚动升级顺序
KRaft 没有 ZooKeeper,升级不再分「先升 ZK 再升 broker」,但顺序依然有讲究。四步:
- 滚动替换二进制。一个节点一个节点地停、换镜像或换包、起。每起一个,等它加入 ISR 再动下一个。controller 节点也在这个流程里,因为 KRaft 的 controller 与 broker 可以同进程。
- 确认全部节点都已换完。用
kafka-broker-api-versions.sh看每个节点报告的服务端版本。 - 升级 metadata.version。换完二进制后集群仍用旧格式运行,这一步才真正切格式。
- 验证并观察。升级后的前几小时盯 URP、ISR 收缩次数、请求延迟。
第 3 步的命令形态是:
docker exec k1 /opt/kafka/bin/kafka-features.sh \
--bootstrap-server k1:9092 --upgrade --metadata 4.3-IV0
这一步不可逆:metadata.version 一旦升上去,回滚到旧版本的 broker 就读不懂新格式了。所以它必须放在所有节点都验证过之后,而不是和二进制替换一起做。
关于多节点滚动升级的完整时序,本机没有实跑——三节点 KRaft 停两个就会失去 controller quorum 多数派,做不出一个「节点逐个替换且集群始终可用」的可信演示。上面给的是命令形态和判据,没有附耗时数据。
升级前必须核对的清单
| 检查项 | 怎么看 | 不通过怎么办 |
|---|---|---|
| 客户端最低版本 | 各服务里 kafka-clients 的版本 | 低于目标大版本减二,先升客户端 |
| 是否用了新协议 | 搜 group.protocol 配置 | 升级前保持 classic |
| 配置文件是否多 listener | 读 listeners 与 advertised.listeners | 每一项都写全,不要依赖继承 |
| 脚本里的工具类名 | 搜 kafka.tools. | 改成 org.apache.kafka.tools. |
当前 metadata.version | kafka-features.sh describe | 记录基线,升级后对比 |
| 备份与回滚方案 | 是否有旧镜像/旧包 | 没有就先补齐,再开始 |
最后一行最容易被省略。KRaft 的 metadata.version 升级之后,旧 broker 二进制读不了新元数据,回滚不再是「把镜像换回去」。真正的回滚只能在升级 metadata.version 之前做。
升级中和升级后盯哪几个指标
升级期间最怕的不是报错,是「看起来正常但正在变慢」。第 15.1 节列的十二个指标里,有三个在升级窗口内要单独拉出来看:
| 指标 | 升级期间的异常形态 | 说明什么 |
|---|---|---|
UnderReplicatedPartitions | 换完一个节点后不归零 | 新节点没追上,别急着换下一个 |
IsrShrinksPerSec | 持续大于 0 | 有节点在反复掉队,通常是网络或磁盘跟不上 |
RequestQueueSize | 换节点过程中持续升高 | 请求堆积,客户端超时即将开始 |
第一个是滚动升级的节流阀。每换一个节点,UnderReplicatedPartitions 应该先升高(该节点的副本暂时掉出 ISR),再回落到 0。在它回落之前不要动下一个节点,否则多个节点同时重建副本,会同时压磁盘和网络,把原本还能用的集群推过临界点。
第三个常被误读。RequestQueueSize 短时升高是正常的,因为被替换的节点上的请求要重新分配。但如果它在换完节点后仍然持续上升,说明剩下的节点扛不住少一个节点的负载——这时候应当回退这次替换,而不是继续往下走。
一句话的取舍
升级本身不难,难的是分清哪些改动可以随时撤回、哪些不能。二进制替换可以撤回,客户端配置可以撤回,metadata.version 不行。把不可撤回的那一步放到最后,并且确保前面每一步都有验证判据,这件事的风险就基本可控了。
下一节做卷一的整体验收:把前面十四个章节的实验汇总成一张表,逐条对照判据。
阅读导航:上一节 15.1 监控指标与告警 | 下一节 15.3 libraryd-mq 卷一验收
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。