先讲个事故
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 年那次,我觉得这个交换是值的。