《Java 消息中间件·Kafka 内核与客户端》15.2 版本升级与兼容性

升级 Kafka 时要同时管住三个版本号:broker 二进制、metadata.version 和客户端 jar。这一节把这三者的关系拆开,给出四组客户端与服务端的交叉实测(含 2.5.1 这种跨越多个大版本的老客户端),并用 KIP-848 新消费组协议演示什么叫「硬下界」,最后给出 KRaft 下的滚动升级顺序与回滚边界。

升级出问题,通常不是「版本不兼容」四个字能解释的

线上做 Kafka 升级,事后复盘最常见的结论是「客户端版本太低」。这句话往往掩盖了真正的原因:可能是客户端用了新协议而服务端不认,可能是某个工具类的包名在 4.x 挪了位置,也可能是新 broker 对配置校验收紧了,旧配置文件原样搬过去起不来。

这三种情况的表现完全不同,处理方式也不同。所以这一节不急着给升级步骤,先把「版本」这个词拆开。

同时存在三个版本号

很多人以为 Kafka 只有一个版本:4.3.0。实际上升级过程中要管住三个互相独立的东西。

名称取值示例谁决定升级时怎么变
broker/客户端二进制版本4.3.0你下载或拉取的包换 jar 或换镜像
metadata.version4.3-IV0集群元数据格式换完二进制后单独执行
inter.broker.protocol.versionKRaft 下由 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。四组结果:

组客户端服务端结果
A4.3.04.3.0成功
B3.9.04.3.0成功
C2.5.14.3.0成功
D4.3.03.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」,但顺序依然有讲究。四步:

  1. 滚动替换二进制。一个节点一个节点地停、换镜像或换包、起。每起一个,等它加入 ISR 再动下一个。controller 节点也在这个流程里,因为 KRaft 的 controller 与 broker 可以同进程。
  2. 确认全部节点都已换完。用 kafka-broker-api-versions.sh 看每个节点报告的服务端版本。
  3. 升级 metadata.version。换完二进制后集群仍用旧格式运行,这一步才真正切格式。
  4. 验证并观察。升级后的前几小时盯 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.versionkafka-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 卷一验收

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《Java 消息中间件·事件驱动架构》附录 B 架构决策记录模板
  2. 《Java 消息中间件·事件驱动架构》附录 A 术语表
  3. 《Java 消息中间件·事件驱动架构》15.3 全书验收