全量发布是上线最危险的一刻。新版本的 bug 会一瞬间砸到所有用户头上,不是慢慢暴露。一次全量发布出了问题,故障面就是全量,回滚要等所有实例重新部署,期间所有用户都在受影响。灰度发布要解决的就是这个风险敞口,把一次大爆炸拆成几次小试探,让 bug 在影响少数用户时就被发现,影响范围小,回滚也快。
很多人把灰度理解成一种发布策略,其实它更像是把发布风险工程化的整套基础设施。它不是按个按钮就能开起来的,背后要流量切分能力、版本标识、实时监控对比、一键回滚这些组件配齐。缺了任何一块,灰度都只是个摆设,出事时还是只能全量回滚。这篇文章讲这套基础设施怎么搭,以及真正容易出事的地方在哪。
三种策略,对应不同的风险预算
灰度发布的策略不止一种,常见的有蓝绿、金丝雀、特征灰度三种,它们对应不同的风险预算和资源成本。
图:蓝绿、金丝雀、特征灰度三种策略在机制、资源成本、回滚方式上的取舍对比
蓝绿发布维护两套完全相同的环境,蓝色承担当前流量,绿色部署新版本。新版本在绿色环境部署好并测过之后,流量整体切到绿色,蓝色待命。出问题就把流量切回蓝色,秒级回滚。蓝绿的代价是双倍资源,你得常年养着一套空闲环境。它的好处是切换是瞬时的全量切换,没有新旧版本共存的过渡期。这种策略适合变更幅度大、需要干净切换的场景,比如大版本升级、数据库迁移这类不适合新旧并存的改动。
金丝雀发布只让少数实例先跑新版本,承接一小部分流量,比如 5%。观察这 5% 流量的错误率、响应时间、业务指标,没问题再逐步扩大新版本比例,5% 到 20% 到 50% 到 100%。金丝雀不需要双倍资源,新版本部署在部分实例上即可。出问题只影响那 5%,回滚就是把流量从新版本实例挪走。这是生产环境最常用的策略,因为它在成本和风险之间取得了最好的平衡。
特征灰度比金丝雀更精细,它按用户特征切流量,而非随机切一部分。地域、App 版本号、用户标签、白名单,命中规则的才走新版本。这种策略的好处是可以精确控制哪些用户先用新功能,比如内部员工先用,再放给报名的灰度用户,再放给 VIP,最后全量。它的代价是网关要支持按规则路由,规则管理复杂度更高。
三种策略不是互斥的,生产里常常组合。用金丝雀做整体比例灰度的同时,再叠加特征规则让内部用户优先。理解每种策略的代价和适用场景,比记住它们的名字更重要。
金丝雀的精髓是早发现
金丝雀这个词来自矿工。早年矿工下井前会带一只金丝雀,井下有毒气体浓度高了,金丝雀先死,矿工看到金丝雀不行了就赶紧撤。灰度发布里的金丝雀同理,新版本的 bug 先在小流量上暴露,不至于一下波及全量用户。
金丝雀发布的核心不在切了多少流量,而在观察什么指标、观察多久。一个常见的失败模式是切了 5% 流量后只看技术指标,错误率没涨、响应时间正常就觉得安全了,全量铺开。结果上线两小时后客服炸了,因为一个业务逻辑 bug 不影响技术指标,却让用户下单失败率翻倍。所以金丝雀阶段要看的不仅是技术指标,还有业务指标,转化率、下单成功率、支付成功率这些直接反映用户体验的数。
观察窗口也要足够长。有些 bug 在低流量下不会触发,比如涉及并发竞争、缓存击穿的,要等流量上去才暴露。所以比例扩大不能太快,每一档至少观察一个完整的业务周期,电商系统至少跨过一个流量高峰,不能只看低谷期的数据。
我的习惯是,金丝雀比例扩到 20% 这档时停留最久,因为 20% 已经能覆盖大部分边界场景,又还没到影响主流用户的程度。20% 稳定跑过一个高峰周期再考虑上 50%。急不得。
流量切分靠网关,靠配置中心
无论哪种灰度策略,落地的技术核心都是流量切分。请求到达网关时,网关读取灰度规则,决定路由到新版本还是旧版本实例。
图:配置中心存储灰度规则,网关实时读取并按规则把流量路由到 V1/V2 实例
规则存在配置中心,网关实时读取,变更实时生效,不需要重新部署。规则可以是 userId % 100 < 5 这种哈希取模切前 5%,也可以是按用户标签、地域、版本号的复杂条件。实例带版本标签,网关根据标签把流量路由到对应版本。
这套机制里最容易出问题的在规则的即时生效,而非网关本身。如果配置中心推送有延迟,灰度规则改了但网关还用着旧规则,就会出现明明已经回滚、却还有用户在走新版本的诡异情况。所以灰度规则的下发要有确认机制,网关确认收到新规则后再继续,否则灰度的可控性就是个空话。
数据兼容性,灰度最容易翻车的地方
灰度期间新旧版本同时在线,这是它和普通发布最大的区别,也带来了最棘手的问题,数据兼容。新旧版本共用一个数据库,schema 变更必须向前向后都兼容。新版本要能读旧数据,旧版本也要能读新版本写的数据。这一点做不到,灰度期间必然出事。
图:DB schema 变更分多步走——先加新字段,并存运行验证,旧版本下线后再迁移数据、删老字段
我见过一次典型事故。新版本把一个字段从整数改成字符串,以为只是自己服务读,没在意兼容。结果灰度上线后新版本开始往这个字段写字符串值,旧版本读到字符串解析成整数直接报错,旧版本负责的那部分流量全挂了。这就是典型的向前兼容没做好的坑。
正确的数据库变更要分多步走,先加新字段让新版本读写,老字段保持不变让旧版本继续用。等灰度完成旧版本下线后,再做数据迁移,把老字段的数据搬到新字段,验证无误后最后删掉老字段。删字段这一步必须在确认旧版本完全下线之后才能做,否则灰度期间一删,旧版本直接读不到字段。
激进地改字段类型、直接删字段,是灰度发布里最危险的操作。这类不可逆的 schema 变更,要么放在灰度之前单独做,要么推迟到灰度完成之后,绝不能塞在灰度版本里。
灰度期间 V1 和 V2 共用一个数据库,schema 变更必须双向兼容。新版本能读旧数据,旧版本也能读新版本写的数据。激进地改字段类型、删字段,会导致旧版本读数据直接报错。DB 变更要分多步,先加新字段,迁移数据,验证,最后等旧版本下线再删旧字段。
回滚要预先想好,尤其是数据回滚
灰度发布最大的价值之一就是回滚快,但回滚快的前提是回滚方案预先想好了。等到出事现场再想怎么回滚,往往已经晚了。
小流量阶段的回滚最简单,直接把流量切回旧版本实例,秒级无损,因为旧版本还在跑。大流量阶段已经把新版本铺到一半了,回滚可以逐步回退比例,也可以一把切回旧版本。一把切回要评估冲击,如果新版本写了大量不兼容的数据,一把切回旧版本可能读不了那些数据,反而更糟。
最棘手的是数据回滚。如果新版本在灰度期间改了数据结构或者做了不可逆的数据变更,回滚到旧版本时这些数据怎么办。比如新版本给一批用户升级了某种状态,旧版本读不懂这个状态,回滚后这些用户就卡住了。所以灰度版本要尽量避免做不可逆的数据变更,能做成可回退的标记位就别真的改数据。实在避不开的,回滚脚本要提前写好测好,不能现场写。
记住一条原则,灰度版本的数据变更越保守越好。能不动的数据就不动,能做成软状态的就别做成硬变更。一旦 V2 改了数据又得回滚,那就是灰度里最折磨人的场景。
功能开关,灰度的轻量补充
实例级的灰度要部署新版本实例,成本不低。如果只是想灰度一个功能,新旧逻辑都在代码里,靠配置开关控制哪些用户走新逻辑,这就是功能开关,也叫 feature toggle。
功能开关的好处是所有实例跑同一套代码,通过配置中心控制开关,比实例级灰度轻量得多,适合功能层面的灰度。运营在后台拨一个开关,指定用户群的新功能就生效,不需要发版。
代价是代码里要同时维护新旧两套逻辑,开关一多代码可读性下降,技术债累积。所以功能开关要有生命周期,新逻辑稳定后要把老逻辑删掉,开关下线,不能让开关长期堆积在代码里。我把这叫开关的卫生管理,定期清理过期开关,否则半年后没人敢删任何一个开关,因为没人知道删了会不会炸。
功能开关和实例级灰度各有适用场景,版本发布用实例灰度更合适,功能层面的快速试验用开关更轻量。两者结合,发布风险就能从粗放到精细地分档控制。
把全量风险拆成可控的小步
灰度发布不是技术炫技,它做的事情很朴素,把全量发布等于全量风险这件事,拆成小步验证等于可控风险。它的价值不在让你发得快,而在让你出事时损失小、回得来。
但要做到这一点,那套基础设施得配齐。流量切分能力是前提,没有网关的规则路由就谈不上灰度。实时监控是眼睛,没有新旧版本指标的对比,灰度就是在盲飞,你看不到那 5% 流量到底有没有问题。一键回滚是安全绳,出事时能秒级切回,而不是重新跑一遍部署流水线。数据兼容性是隐形的地雷,schema 变更不做好向前兼容,灰度期间新旧版本一起崩。
把这四块想透、搭稳,灰度发布才真的可控。任何一块偷懒,灰度就退化成一次心理安慰版的发布,出事时和全量发布没有任何区别。上线这件事,谨慎永远比速度值钱。








