3.4 灰度发布:按调用方放量
场景:全量发布就是全量冒险
2.2 节做过预热,这节正式讲灰度。我们吃过全量发布的亏:新版库存接口周四晚上直接替换旧版,结果一个边界条件下排序逻辑变了,三家客户的报表全错,全员回滚到凌晨。事后复盘:为什么不能先放给两家配合度高的客户跑两天?答案是当时没有灰度能力,只有发布开关。灰度模块就是为了把发布的爆炸半径从全站缩到一两家。
灰度的核心诉求是按调用方放量,而不是按流量比例。按比例切只能验证系统层面能不能跑,按调用方切能验证具体业务场景对不对——这是我们的发布哲学:先验证正确性,再验证承载力。
方案:名单、比例、回滚三件套
灰度的完整生命周期被我们拆成五个阶段,每个阶段都有明确的退出条件,谁也不敢跳阶段(跳过的事我们都干过,都被回滚教过做人):
| 阶段 | 放量范围 | 观察时长 | 退出条件 |
|---|---|---|---|
| 内部验证 | 内部 appkey | 半天 | 零错误 |
| 试点 | 白名单 2~3 家 | 1 天 | 业务方确认正确 |
| 小批量 | 白名单加十家 | 1 天 | 错误率持平 |
| 百分之三十 | 按名单分片 | 1 天 | 各项指标持平 |
| 全量 | 全部 | 持续 | 进入正常监控 |
灰度配置存在 Redis 的有序集合里,key 里带灰度版本号,与 3.3 节的缓存版本号联动变更,灰度一变缓存自动失效。回滚是灰度的另一半:任何阶段异常,一键把名单清回上一版本,时间在三分钟以内——前提是你提前演练过,回滚动作从首次演练到肌肉记忆,不能等出事再读文档。
按调用方灰度的另一层考虑是追责清晰。按流量比例切时,出问题的客户不知道自己是被选中的倒霉蛋,投诉时双方都在猜。按名单切则明明白白:试点客户提前被告知,出了小问题双方都有预期,协作关系反而更好。灰度发布某种程度上也是客户预期管理的工具。
灰度五阶段表贴在墙上只是第一步,真正让阶段不被跳过的是把退出条件做成自动检查。我们给每个阶段配了一个 CLI 校验命令:执行后自动拉取灰度窗口内 beta 组的错误率、P99 延迟与业务确认工单状态三项,全部达标才允许进入下一阶段。想跳阶段?命令自己会拒绝执行并把不达标的原因打印出来——把规则写进工具,比写在 wiki 里等大家自觉可靠得多。
配置:灰度路由核心
灰度判定的实现就是一个查名单逻辑,但它挂在 2.1 节的路由分组上。命中试点名单的请求打 beta 路由,其余打 stable。这段代码简单到可以一行一行 review:
<?php
// 灰度中间件核心
public function pickGroup(string $appkey, string $grayId): string
{
// 版本废弃提示(配合 2.1 节路由)
$rank = $this->redis->zrevrank("gray:{$grayId}", $appkey);
if ($rank !== null && $rank < $this->batchSize) {
return 'beta';
}
return 'stable';
}
灰度用的有序集合按提前规划的批次有序写入,zrevrank 的排名与 batchSize 比较即实现按批次分片放量。灰度的下一步与熔断恢复期的爬坡(见 3.2 节)共用同一思路:任何变化都要渐进,而不是开关一把梭。CLI 工具提供 add、batch、rollback 三个命令,操作全部走 git 留痕。为什么不用界面化控制台?因为灰度操作频率低、影响大,git 留痕加 PR review 的摩擦力恰好是合适的——界面按钮太容易手抖点错,而一个需要两人 review 的 PR 能挡住百分之九十九的冲动操作。
批次写入还有个细节:有序集合的 member 是 appkey,score 是入库顺序。灰度计划表先在内部评审会上定好分批顺序,再由脚本批量写入。中途想临时插入一家客户,不允许直接改集合,而是改计划表重跑,保持集合内容永远可由计划表推导。单一来源原则在灰度这种高风险模块上尤其重要。
灰度期间还有个实用设计是新旧版本双跑对比。对查询类接口,beta 路由的响应与 stable 路由的同参数结果做异步 diff(抽样百分之五,只记录不一致的字段名不记录值)。灰度结束前 diff 报告清零是全量的前置条件之一。这个机制帮我们抓到过好几次隐性差异:返回值结构没变,但某个排序字段的稳定性变了,下游客户的报表会平添噪声。
灰度与回滚之外,我们还把这套机制扩展到了配置变更:限流参数调整、契约变更、阈值改动,全都走同一套灰度通道。任何让系统行为变化的输入都是发布,区别只是它叫代码还是叫配置。配置变更灰度化之后,契约改错导致的事故降到了零。
踩坑:灰度里翻过的车
第一个坑是灰度与缓存的串号,3.3 节详述过:灰度名单变了缓存没变,等于灰了个寂寞。从那以后灰度变更与缓存失效打包成一个原子操作。第二个坑是试点客户不具代表性:试点名单里一家的业务场景过于简单,全量后大客户的复杂场景翻车。选试点客户的硬标准改为覆盖三类场景:高并发型、长尾参数型、边缘条件型,由产品同学按客户画像勾选,不是谁配合选谁。
第三个坑是灰度期间指标对不上。试点只有三家客户时,全局错误率几乎不动,灰度问题被平均掉了。修复是监控面板按灰度组分别出曲线,beta 组单独一套告警阈值(更严),而不是看全局大盘。监控不分组,灰度等于蒙眼。第四个坑是紧急修复合规:有次线上事故,同事绕过灰度直接全量推修复,结果修复本身有 bug,又得回滚。制度补丁:任何变更都走灰度通道,紧急修复也不例外,只是把阶段间隔从天压缩到小时。流程是保命的,越急越不能绕过它。
第五个坑是灰度期间的文档同步。灰度中的新接口,文档站上对非试点客户不可见,试点客户走专属文档分支。曾经发生过新接口文档对全量客户可见但接口只在灰度,非试点客户照文档调用得到 404,客服压力巨大。修复是文档站与灰度名单打通:看文档的权限本身跟着灰度走,试点结束全量发布时文档才解禁。这是把技术与运营联动做得最细的一个教训。
第五个坑是灰度期间名单的陈放问题。某次灰度持续到第二周,名单里一家客户悄悄把调用方式从短连接改成了长连接复用,beta 组的代表性在不知不觉中变了,最后一批放量时结论失真。修复是灰度超过三天必须重新评审试点名单,确认试点客户的调用形态没有漂移。灰度时间越长变量越多,这条纪律是给灰度结论保鲜。
灰度发布的数据沉淀也有意外收获。每次灰度的退出条件是错误率持平,长期积累下来我们知道每个接口的历史错误率基线,哪个接口天生错误率偏高一目了然。某次新接口灰度时错误率百分之零点五,看似很低,但同接口历史基线是零,值班同学据此发现了边界条件的参数校验遗漏。没有基线就没有判断,监控与灰度共享同一套数据资产。
另外说个朴素但重要的实践:灰度计划必须在工单系统里有个对应任务。灰度不是工程师的个人行为,客服需要知道这几天哪些客户是试点(他们的工单优先级更高),商务需要提前给试点客户打招呼。技术系统外的协同做不到位,灰度过程的反馈链就断了,试点客户就成了被遗忘的免费 QA。
最后给个可复用的检查单:发布前确认回滚脚本、监控分组、试点名单、文档可见性四项齐备。四项里缺任何一项,这次发布就值得再想想。这套单子我们打印贴在工位上,庸俗但有效。
顺带分享一次教科书式的灰度。去年我们重构了整个库存查询接口,一次颇有魄力的改动。灰度第一批选了一家高频但场景简单的客户,第一天就发现慢查询缓存的序列化格式变了,旧客户端反序列化诸报错——立刻 rollback,修复后重试。第二批加入一家边缘条件型客户,又发现一个参数兼容问题,再次回滚修复。要是按老办法全量推,这两次事故会叠加在四十家客户身上。灰度把两次炸弹拆成了两次哑火,团队周五准时下班。这就是这套机制存在的意义。
灰度机制还能反过来救下其它系统。有一次后端同学要重构数据库表结构(危险动作),我们直接拒绝了停服全量切换的方案,改为新旧表并行双写加灰度切读:前百分之十读新表,观察一天无问题再逐批放大,全程随时可回退到旧表。这次切换历时一周零事故完成,数据库团队从此成了灰度发布机制的头号粉丝。技术上能可逆的变更就不要做成不可逆,灰度机制就是那扇可以反复推开的门——它把高风险的终点冲刺,变成了一串可以随时刹车的小步快走。
灰度发布做深之后,我们把它抽象成了内部的一句话原则:任何让系统行为变化的输入,都值得拥有自己的灰度通道。代码发布、配置变更、机器扩容、依赖升级、证书轮换,只要是变化就能灰度。这套思维普及开后,全公司的事故曲线整体下移了一个台阶,而灰度机制本身的执行成本反而越来越低——流程一旦形成习惯,它就不再是成本,而是速度的一部分。新来的同学第一周就学会这套语言,因为他们看到所有老人都在这么做,文化就是这样自然长出来的。
小结
按调用方灰度的三板斧:白名单名单加批次有序集合、灰度组独立监控曲线、三分钟回滚外加季度演练。灰度发布降低的是变更风险,但真正让它生效的是纪律——阶段不跳、名单不擅自扩、紧急修复也走通道。发布工程师的最高荣誉不是推了多少代码,而是推了几年的代码从来没炸过。灰度让发布从赌博变成实验,从玄学变成工程。
最后强调一个组织前提:灰度机制能跑起来,靠的是团队愿意接受摩擦。快速通道人人都想要,但灰度通道的意义恰恰是让快变慢一点。我们的做法是只保留一条合法快车道——真正紧急的修复,阶段间隔压缩到小时级,但通道必须走完。妥协的不是流程而是时长,这条边界守住了,灰度机制才不会在某次赶工中名存实亡。
