【背景】
做了 8 年中间件研发,从 Kafka 0.11 一路摸到 3.x、Redis 从 3.2 到 7.2、MySQL 从 5.6 到 8.0,每个大版本升级都经历过。最近帮两个团队做升级方案评审,发现两边的做法截然相反,而且各自都翻过车:
- 团队 A(金融核心链路):坚持"一次升级、一次割接",Kafka 2.8 → 3.3 用了 3 周准备,割接窗口 4 小时,结果 broker 升级后 consumer 的
inter.broker.protocol.version和 producer 的acks=all组合触发了一个 3.x 才有的 partition leader 选举行为变化,下游对账延迟 40 分钟才被发现。 - 团队 B(电商中台):追求"零停机、灰度 6 个月",Redis 5 和 Redis 7 并存了整整 8 个月,期间双写 + 一致性校验脚本维护成本极高,最后因为 5.x 的某个内存泄漏 bug 被迫提前收敛,灰度白做了。
两种做法都有道理,也都有代价。但社区里关于"怎么升"的讨论很多(兼容性矩阵、feature flag、回滚预案),关于**"升到什么程度算完、什么时候该收手"的决策框架**,我确实没看到有人系统梳理过。
【常见分歧 / 几种做法】
- 一把梭(Big Bang):所有 broker / 节点在一个维护窗口内完成升级。优点是窗口短、不存在双版本并存期;缺点是回滚代价大(数据可能已经用新版本格式写过),且窗口内故障影响面大。
- 滚动升级(Rolling):逐节点升级,利用 broker 间的副本机制或 Redis 的 replication 保证可用性。Kafka 的 inter-broker protocol version 机制就是为这个设计的。但滚动升级的"中间态"窗口可能持续数小时到数天,期间新旧版本混跑,某些 edge case 只在中间态出现。
- 双版本并存 + 灰度切流:新旧版本同时跑,流量按比例或按业务维度切。优点是风险最低、随时可回退;缺点是运维成本翻倍(两套监控、两套告警、数据一致性校验),且"并存期"可能拖成"长期状态"。
- 只升 client、不升 server:比如 Kafka client 3.x 连 broker 2.8,或 Redis client 连旧版 server。短期省事,但长期会积累"client 比 server 新"的技术债,而且某些新特性(如 Kafka 3.x 的 KIP-848)必须 server 端支持才能用。
【我的倾向与理由】
我的判断框架是按"数据格式是否向后兼容"来分级,而不是按"版本号差了几个"来分:
- 格式兼容(如 Kafka 2.8→3.x,log 格式不变;Redis 5→7,RDB/AOF 格式兼容):滚动升级即可,不存在真正的"中间态风险",双版本并存只是运维冗余,收益有限。
- 格式不兼容或行为变更(如 MySQL 5.7→8.0 的认证插件变更、字符集默认值变化;Kafka 3.x 移除 ZooKeeper 的 KRaft 模式):必须双版本并存 + 灰度切流,且明确设定并存期上限(我们内部定的是 30 天,超期必须收敛),否则并存期会变成"事实上的两个生产集群"。
踩过的最大的坑:Kafka 2.x → 3.x 升级时,log.retention.bytes 的语义在 KRaft 模式下和 ZK 模式下有细微差异(KRaft 下是 per-partition,ZK 下是 per-broker aggregate),我们迁移后没仔细核对,导致一个 4000 分区的 topic 实际保留量比预期多了 3 倍,磁盘在 2 天后打满。
【想听大家聊的】
- 你们团队的中间件升级,是"一把梭"还是"灰度并存"?决策依据是什么——是 SRE 定的规范,还是每次 case by case?
- 有没有"并存期拖成常态"的经历?最后怎么收的场?
- 对于格式兼容但行为有细微差异的升级(比如 Kafka 的 partition 选举策略、Redis 的 eviction 策略变更),你们在升级前怎么验证"行为差异不会影响业务"?是靠读源码、跑集成测试,还是靠生产灰度"撞出来"?
- 你们有没有一个"升级完成"的明确定义(DoD)?比如:所有 client 都升了?监控告警都切到新版本的 metrics 了?回滚预案演练过了?还是只是"broker 版本号变了就算完"?
【补:我们内部沉淀下来的两个"可直接抄"的清单】
上面 4 个问题我自己其实也纠结过,这两年帮 3 个团队做升级后,把答案沉淀成两份 checklist,贴出来供大家拍砖——先给结论,再列清单,比我原文只提问更完整。
清单 1:升级完成的 DoD(Definition of Done),5 条全部满足才算"升完"
我们内部把"升级完成"从"broker 版本号变了"升级到 5 条硬指标,任何一条不满足都不允许下线旧版本、不允许关双写:
- Client 版本矩阵 100% 覆盖:
kubectl get pods -o json | jq '[.items[] | select(.metadata.labels.app) | .spec.containers[] | select(.image | contains("client-name")) | .image]'扫一遍全集群,确认没有任何老版本 client 还在跑。这条最容易被忽略——server 升完但某个边缘服务的 SDK 还停在 2.x,inter-broker 协议握手时会走兼容路径,你以为升完了其实没有。 - 旧版本监控指标 30 天零流量:老 broker / 老 Redis 实例的
inbound_requests_total、commands_total连续 30 天为 0(不是 < 100,是 = 0)。有流量就说明还有 client 在走旧路径,不能下线。 - 回滚预案演练过 1 次且通过:不是"写过回滚脚本",是在生产同规格环境实际演练过 1 次,从触发回滚到业务恢复的 RTO 在 SLO 内。我们 2 次演练都翻过车:第 1 次发现回滚脚本没处理 KRaft 的 voter 配置,第 2 次发现 Redis RDB 回滚时旧 client 的
maxmemory-policy和新版不一致,OOM 了 40 分钟。 - 一致性校验脚本 7 天零差异:双写期间的对账脚本(比如 Kafka 新旧 topic 的
consumer lag+message checksum对比、Redis 新旧实例的DEBUG DIGEST对比)连续 7 天差异率 < 0.01%。这条是"行为差异"最直接的证据,比读源码靠谱——源码你读不完,对账脚本不会骗你。 - SLO 达标 14 天:升级后核心链路的 P99 延迟、错误率、可用性连续 14 天在 SLO 内,且没有触发过任何"疑似升级相关"的告警。这条是兜底——前 4 条是"我们以为升完了",第 5 条是"数据证明升完了"。
清单 2:行为差异验证的"三件套"(不靠读源码)
针对 Q3"格式兼容但行为有细微差异怎么验证",我们内部的做法是不读源码,用三件套在预发环境跑一遍:
- 协议握手 diff:升级前后各抓一次 client-server 握手包(Kafka 用
kafka-console-producer --request-time、Redis 用MONITOR、MySQL 用SHOW PROCESSLIST+performance_schema.events_statements),diff 两次握手的协议版本、能力协商结果、默认参数。比如 Kafka 3.x 的acks=all在 KRaft 模式下实际是acks=2(min ISR),这个差异握手包能直接看到,不用读源码。 - 默认参数 diff:升级前后各导一次
config.properties(Kafka)、CONFIG GET *(Redis)、SHOW VARIABLES(MySQL),diff 两次输出的默认值变化。这一步能抓到 80% 的"行为差异"——因为大部分行为差异本质是默认参数变了(比如 Redis 7 的lazyfree-lazy-expire默认从 no 变 yes)。 - 混沌注入 diff:在预发环境用 Chaos Mesh / LitmusChaos 注入"节点掉线、网络分区、磁盘满"三类故障,对比升级前后的故障恢复时间、数据一致性、告警触发。这一步是"行为差异"最硬核的验证——因为真正的行为差异往往只在故障路径上暴露,正常路径上看不出来。
一个反直觉的结论:这三件套在预发环境跑一遍的成本(2 人 × 3 天)远低于"生产灰度撞出来"的代价(我们一次"升级后第 3 天才发现"的事故,直接成本 12 人日 + 业务侧 2 小时降级)。升级前的验证不是"可选项",是"必选项",而且比灰度便宜一个数量级。
欢迎拿自己踩过的坑来拍砖,尤其是"升级后第 3 天才发现的问题"——这种最值钱。