灰度、蓝绿、滚动发布到底怎么选?一次凌晨事故后,我把发布指标全改了

🔑 关键词:版本发布, 灰度发布, 蓝绿发布, 数据库迁移, 回滚时间

📖 摘要:从一次凌晨的线上事故出发,对比灰度、蓝绿、滚动三种发布策略的真实参数和适用场景,聊一个被大多数人忽略的发布核心指标——变更的原子性覆盖度,以及 expand-contract 数据库迁移的完整步骤。

上个月凌晨一点二十,我在公司楼下便利店买关东煮,手机响了。告警群里说订单服务的 P99 从 180ms 涨到 4.2s。那天晚上我们发了一个看起来无比安全的版本——只加了一个复合索引,业务代码改了三行。结果 MySQL 8.0 在 8000 万行的订单表上做 DDL,所谓业务低峰期也不低峰,锁等了 6 分钟,连接池直接被打满。回滚代码用了 2 分钟,helm rollback 一条命令的事。但那个索引删不掉,DROP INDEX 一样要重建二级索引,我们最后是等到两点多才把流量导回主库。整个过程 47 分钟。

图片

那天之后我改了一个想法。我们花了两年时间把发布频率从每周一次提到每天七八次,CI 流水线从 22 分钟压到 6 分半,镜像瘦身到 380MB,每个人都在为「发布更快」鼓掌。但我们从来没认真量过一次回滚要多久,更没量过有多少变更根本回不去。

三种发布策略,参数比名字重要得多

图片

先说灰度(金丝雀)。按比例导流,我们用的是 Istio,VirtualService 里权重走 1 → 5 → 20 → 50 → 100,每一档至少观察 10 分钟,盯 5 个指标:HTTP 5xx 率、P99、容器 CPU、JVM GC 次数、DB 慢查询条数。这套东西的前提是你的流量能按用户维度路由,如果服务里有内存态(比如本地缓存、WebSocket 会话),用户在新旧版本之间来回切是会丢状态的,这点很多人上线之后才发现。

蓝绿发布是两个完整环境,切流量。它最大的优点是回滚极快——把 LB 的 upstream 切回去,10 秒内。代价是机器。我们当时 68 台 8C16G,全量蓝绿等于多留一倍,一个月多花大概一万二。对预算紧的团队,这个数字比任何架构图都有说服力。

滚动更新是 K8s 默认的,maxSurge=25%、maxUnavailable=25% 起步。但严格说它不算发布策略,只是部署策略:它不控制流量灰度,新 Pod 一起来就直接接全量请求。我见过好几个团队把它当灰度用,然后奇怪为什么「灰度」的时候用户还是报错——因为根本没有按比例分流,只是分批替换而已。

图片

真正难的部分是数据库,不是代码

代码是可逆的,schema 基本不可逆,或者代价高到你不愿意付。我们后来统一用 expand-contract 模式,步骤是这样的:第一个版本只加新列,允许 NULL,同时代码双写新旧两列;第二个版本回填历史数据,分批做,每批 5000 行,控制在 100ms 以内;第三个版本切换读路径到新列,观察一周;第四个版本停掉旧列的写入;第五个版本才删旧列。那个订单表加字段,从第一个版本到删掉旧列,前后走了 11 天,跨了 6 次发布。

图片

听起来很重,但对比一下凌晨那 47 分钟,我愿意每次都走完。而且每一步都能单独回滚,这才是关键。

所以我现在衡量一个发布系统的核心指标不是发布频率,而是「变更的原子性覆盖度」:你能量化出多少比例的变更是可以一键原子回滚的?如果是 100%,你随便发,一天发 50 次都没事。如果只有 60%,剩下那 40% 每次发布都在赌。我们团队去年这个数字大概在 55%,现在推到 85%,靠的全是拆发布、拆迁移、拆 flag,跟 CI 提速一点关系都没有。

图片

几个具体到有点琐碎的做法

Feature flag 当保险很香,但它会欠债。我们最多的时候代码里有 340 个 flag,一年后清理到 90 个左右。清理 flag 本身也是一次发布,要排期,要测试,没有人喜欢干这个,但不干迟早出事。

版本号我们早就不用语义化那套了,内部服务直接上 git commit sha 前 7 位加日期,比如 20240612-3f8a9c1,部署的时候一眼能看出是哪次提交,比 v2.3.1 有用得多。

图片

发布窗口避开三样东西:整点(定时任务扎堆)、月初(财务对账)、周五下午(出事没人)。这三条听着像迷信,但都是踩出来的。

最后是回滚演练。每季度挑一个服务,假装它挂了,计时看多久能恢复。我们第一次做的时候发现有个服务的 helm chart 半年前就没人维护了,回滚直接失败,那个 chart 里的镜像 tag 指向的仓库已经删了。这事要是在真实故障里发生,就是几个小时的 downtime。演练比任何文档都能暴露问题。

🏷️ 标签: