版本发布多久一次才不痛苦?对比 Chrome 4周、K8s 15周、Ubuntu 26周,附我们的灰度与回滚参数

🔑 关键词:版本发布,灰度发布,SemVer,发布节奏,回滚策略

📖 摘要:从一次真实的 4.0.0 发版事故讲起,对比 Chrome、Kubernetes、Ubuntu、Node.js 的发布周期差异,讨论 SemVer 在 App 场景下的误用,并给出我们团队跑了两年的一套发布流程和灰度、回滚具体参数。

先讲个事故

图片

2022 年 11 月 8 号晚上,我们 Android 客户端从 3.8.2 直接跳到了 4.0.0。

那次之所以敢跳大版本,是因为攒了三个多月的东西:信息流卡片改版、登录 token 的刷新逻辑重写、埋点 SDK 从 v1 换到 v2。三个改动分属三个小组,各自都测完了,合到一起在测试环境跑了一周没出问题,大家心里都挺踏实。

11 月 9 号上午 10 点,灰度 10%,投放 3 万台设备。11 点 20 分,Firebase Crashlytics 上的崩溃率从 0.12% 涨到 1.7%——我记得特别清楚,因为当时我就站在工位看板前面,那个数字是跳着往上走的。

根因是 token 刷新逻辑和埋点 SDK 的初始化顺序打架了,只在冷启动加弱网加已登录这三个条件同时成立的时候才复现。测试环境的弱网模拟器只跑了 2G 场景,没跑 3G。

回滚花了 47 分钟。麻烦的地方在于 Google Play 已经把 4.0.0 推给那 10% 的用户了,没法撤回,只能发 4.0.1 覆盖,中间这段时间用户打开就闪退。

后来复盘,结论其实就一句:发布出事故,通常不是发布这个动作做错了,而是一次发的东西太多了。

三种节奏,本质是把痛苦分给谁

图片

先看几张真实的时间表,都是能查到的:

项目 发布周期 一个版本塞多少 升级痛苦谁扛
Chrome 4 周(2021 年 9 月 Chrome 94 开始,此前是 6 周) 小 用户,后台静默更新基本无感
Firefox 4 周(2020 年 1 月 Firefox 72 开始) 小 用户
Kubernetes 约 15 周,一年 3 个 minor 中 运维,1.28 是 2023 年 8 月 15 日发的
Ubuntu 26 周一次,每年 4 月和 10 月,LTS 每 2 年 大 桌面用户和运维
Node.js 偶数版本 4 月进 Current,10 月转 LTS,维护 30 个月 大 开发者,Node 20 是 2023 年 4 月 18 日发布,10 月 24 日转 LTS
Windows 每年 1 次功能更新 很大 所有人

这些日期我都是照着官方 release notes 抄的,不敢凭记忆写。

看完这张表你会发现,节奏快的项目都有一个共同点:它们有办法把新版本悄悄塞给用户。Chrome 能做到 4 周,是因为它有静默更新通道,有分阶段放量,还有 field trial 机制——每次发布同时跑几百个 A/B 试验,某个特性崩了可以单独关掉,不用回滚整个版本。

你要是没有这套东西,学它 4 周发一次,纯粹是给自己找事。

反过来看 Ubuntu 的 6 个月。它不是不想快,是内核和驱动没法静默重启。用户机器上装着 NVIDIA 闭源驱动、老式打印机驱动、各种 DKMS 模块,动一个就可能黑屏。这是物理约束决定的节奏,不是团队能力决定的节奏。

所以我的看法是:发布周期没有最优解,你只是在选择把成本转嫁给谁。转嫁给用户,你就得有静默更新的能力;转嫁给运维和开发者,你就得给出足够长的兼容窗口和迁移文档。最怕的是两边都不想得罪,攒半年发一次,然后发出来一地鸡毛。

图片

版本号:SemVer 是给库用的,不是给 App 用的

再说版本号,这块我们内部吵得最凶。

有一年为了 4.0.0 这个号该不该发,开了三次会。我当时去翻了 SemVer 2.0.0 的规范原文,它写得很清楚:主版本号在不兼容的 API 变更时递增,次版本号在向下兼容的功能新增时递增,修订号在向下兼容的 bug 修复时递增。

问题就在这——App 有 API 吗?

没有。App 的用户是活人,活人不会 import 你的首页,也不会因为你的按钮从左边挪到右边就编译不过。

所以我现在给团队的建议是按场景分:

对外 SDK、npm 包、公共 API —— 老老实实 SemVer,主版本号能不加就不加。React 从 17(2020 年 10 月)到 18(2022 年 3 月)隔了快一年半,就是因为 18 引入了 concurrent rendering 这类破坏性变更,加主版本号有正当理由。反过来,如果你只是加了个可选参数,那就老老实实加次版本号,别整活。

图片

自研 App、SaaS —— 我倾向用 CalVer,比如 2024.11.3;或者把 marketing version 和 build number 拆开,商店里展示 5.2,内部构建号是 20241109.1。用户不关心语义,只关心这是不是最新的。

内部微服务 —— 用 commit hash 或者日期,别用语义化版本。内部服务之间本来就该同步升级,搞一套 SemVer 只会让人误以为可以跨好几个版本兼容。

顺便说个反例。我见过一个团队改了登录页的按钮颜色,发了 2.0.0,理由是视觉变化很大。结果下游三个接入方以为接口不兼容,连夜开会排查了两小时。

版本号是合同,不是心情。

我们现在的做法,附具体参数

踩了两年坑之后,我们把流程固定成这样,你可以直接抄:

图片

1. 固定节奏:每 2 周一个窗口。 周三下午 3 点发版,周一周二冻结代码。为什么是周三?因为周二发完如果炸了,你还得熬一个通宵;周四发完炸了,周五没人愿意加班修。周三下午发,出问题有完整的周四白天可以处理。

2. 分支策略:release 分支发版前 24 小时锁定。 锁定之后只允许 cherry-pick 带 P0/P1 标签的修复,每个修复需要两个人签字。这条规矩救过我们好几次。

3. 版本号: marketing version 用 CalVer,形如 2024.11.3,build number 交给 CI 自增,开发者不许手动改。

4. 灰度五档: 1% → 5% → 25% → 50% → 100%,每档停留至少 30 分钟。iOS 这边更省事,App Store 的分阶段发布是固定的 7 天曲线,官方文档写的是第 1 天 1%、第 2 天 2%、第 3 天 5%、第 4 天 10%、第 5 天 20%、第 6 天 50%、第 7 天 100%,中间可以随时暂停,最多暂停 30 天。Android 的 Google Play 可以手动设百分比,我一般 5% 起步。

5. 回滚阈值,三个硬指标: 崩溃率超过 0.5%(我们基线是 0.12%)、核心接口错误率超过 1%、首屏加载 P95 超过 2.5 秒。任意一个触发,无条件全量回滚,不许讨论。

6. 客户端必须有 kill switch。 服务端回滚只是改个配置,客户端回滚只能发新版本,所以每个新功能上线时都要带一个远程开关,默认关闭,出问题 30 秒内关掉。

7. 每次发版归档 release note。 不写给用户看的营销文案,写给三个月后的自己:这次改了什么、动了哪些依赖、灰度过程有没有异常。

图片

还有一件事:有时候节奏不是你定的

2023 年 8 月 31 日之后,Google Play 要求新提交的应用 targetSdkVersion 至少是 33;2024 年 8 月 31 日这条线抬到 34。你不升,就不能更新应用。

这就是外生节奏。你的发布计划里必须给这些别人定的 deadline 留出至少一个季度的缓冲,因为 targetSdkVersion 升级往往会带出一串权限和后台限制的适配问题,不是改个数字就完事。

最后

我现在越来越觉得,发布这件事的核心不是多久发一次,而是一次能安全地发多少。

Chrome 4 周发一次,是因为它一次只发一点点,而且有自动回滚。你 2 周发一次但每次都塞 30 个需求,那和半年发一次没区别,只是把事故频率提高了 12 倍。

我们现在的 release train 上,一个版本平均只带 6 到 8 个需求。听起来很慢,但过去一年我们没有再出现过一次超过 10 分钟的线上故障。比起 2022 年那次,我觉得这个交换是值的。

🏷️ 标签: