主题
蓝绿灰度与多级路由
一句话:蓝绿是整站换环境,金丝雀是按比例放量,全链路灰度是按请求打标签并在每一跳独立选版本;回滚靠流量和配置切回,不靠熔断。
适用:Spring Cloud Gateway、Consul 1.16+、Apollo 2.x、Sentinel 1.8+;Kubernetes 侧的发布机制见 03-灰度发布与交付平台。
为什么需要
新版本不能一次性推给所有用户。出问题时,希望影响面可控、回滚足够快;验证时,希望真实流量能走到新版本,而不是只看测试环境。
蓝绿、金丝雀、全链路灰度解决的是不同粒度的问题:整站切换、单服务放量、跨多个服务的联动变更。混用概念会导致「以为在灰度,其实一半流量已经走了新链路」。
核心机制
三种发布策略
| 蓝绿 | 金丝雀 | 全链路灰度 | |
|---|---|---|---|
| 粒度 | 环境级,整站切换 | 服务级,按比例 | 请求级,按标签 |
| 新旧共存 | 通常不共存 | 共存 | 共存 |
| 回滚 | 切回旧环境 | 比例拉到 0 | 关闭染色或摘除灰度实例 |
| 适用 | 核心系统整体切换 | 单服务逐步放量 | 多服务联动变更 |
蓝绿需要双倍资源,但切换和回滚最干脆;金丝雀资源成本低,但只控制一个服务的比例;全链路灰度最灵活,也最依赖每一跳都支持标签透传。
请求级灰度的五个环节
一条请求走完灰度,要经过:染色 → 透传 → 路由 → 生效 → 回滚。
- 染色:网关按规则给请求打
x-gray-tag。规则可以来自白名单、用户哈希、Header 或随机比例。 - 透传:HTTP Header、线程上下文、MQ 消息属性都要带标签。线程池必须用 TTL(TransmittableThreadLocal)包装,否则异步线程拿不到标签。
- 路由:每一跳的 LoadBalancer 读取注册中心的灰度元数据,按标签选实例;没有匹配实例时回退稳定版。
- 生效:规则热更新,不重启;实例上下线由注册中心感知。
- 回滚:关闭染色、摘除灰度实例、切回稳定版。回滚是流量动作,不是代码动作。
五种灰度场景
| 场景 | 标签 | 灰度实例 | 说明 |
|---|---|---|---|
| 单服务 | v2-b | B | 只升级 B |
| 相邻联动 | v2-ab | A、B | A 和 B 接口一起变 |
| 跨跳联动 | v2-ac | A、C | 中间 B 保持稳定 |
| 全链路 | v2 | A、B、C | 大版本整体验证 |
| 链尾 | v2-d | D | 只改下游末端 |
核心规则只有一条:请求的标签是否属于实例注册的灰度标签集合。一个服务的灰度实例缺失时,回退稳定版,标签继续往下传;否则一个中间服务没升级,整条灰度链就断了。
标签透传的三个通道
- RPC:HTTP Header
x-gray-tag,Feign 拦截器自动注入。 - 线程:
TransmittableThreadLocal+TtlExecutors包装线程池;线程池不包装,异步任务就丢标签。 - MQ:Producer 写消息属性,Consumer 读取后恢复上下文;灰度消费者用独立 ConsumerGroup,避免和稳定版抢消息。
定时任务要显式跳过灰度实例,否则灰度版本会在后台跑批,影响面不可控。
回滚与熔断不是一回事
回滚是把流量切回稳定版:改配置停止染色、从注册中心摘除灰度实例、必要时重启实例。
Sentinel 的熔断是当错误率或慢调用比例超过阈值时,拒绝请求并走降级逻辑,它不会把部署回退,也不会自动把流量切回旧版本。熔断恢复是半开探测,和发布回滚是两个动作。把熔断写成「自动回滚」会让人误以为有新版本回退能力,实际只是拒绝。
共享资源的风险
灰度实例和稳定版通常共享数据库、Redis、MQ。灰度流量打满连接池或 Redis 带宽,稳定版会一起抖动。所以灰度发布要同时看共享资源指标,并对灰度流量单独限流;数据库 schema 只能做向下兼容的变更,先加字段、再双写、最后切读,不能直接删改。
最小上手
放在 Apollo 配置项里的灰度规则(应用自定义结构,不是 Apollo 原生 schema):
yaml
scenarios:
full-gray:
tag: v2
services: [A, B, C]
match: { strategy: user_hash, ratio: 0.05, priority: 50 }
ab-gray:
tag: v2-ab
services: [A, B]
match: { strategy: whitelist, users: [10001], priority: 10 }请求侧验证:
bash
curl -H 'x-gray-tag: v2' https://api.example.com/order/123LoadBalancer 选择逻辑(伪代码):
text
tag = GrayContext.get()
if tag == null: return choose_stable(instances)
matched = filter(instances, instance.gray_tags contains tag)
return matched.empty ? choose_stable(instances) : lb.choose(matched)观测:TraceID 与 grayTag
TraceID 串起一次调用,grayTag 决定每一跳走哪个版本。两者都放在 Header 和日志里:排查时先按 TraceID 找到整条链路,再看每跳的 grayTag 和实例版本,就能定位是哪一跳丢失或路由错。跨语言传播用 W3C traceparent,MQ 消息属性和异步线程也要透传。
生产动作
- 规则优先级:白名单高于比例规则;一个用户命中多条规则时按 priority 取第一条,避免规则叠加。
- 比例稳定性:按用户哈希分桶,不要每次请求随机,否则同一用户会在新旧版本之间跳。
- 监控:灰度流量占比与配置的偏差、灰度链路完整率、灰度/稳定版 P99 比值、错误率、标签丢失率。
- 回滚顺序:先关染色(秒级),再摘灰度实例(分钟级),最后才考虑重启;每一步观察稳定版是否恢复。
- 数据库:只做加法;新增字段给默认值;删除字段要等所有版本都不再读写。
- MQ:灰度消息用独立 ConsumerGroup;消费失败要能回到稳定版处理,不能把消息卡在灰度组。
- 定时任务:灰度实例默认跳过,需要验证时用单独的任务分组。
边界
- 全链路灰度要求所有跳都支持标签;任何一跳丢失,后面的灰度就不完整。
- 共享数据库和 Redis 会让灰度故障扩散到稳定版;资源隔离比标签路由更难,也更关键。
- 按比例灰度不等于按用户稳定分流;没有哈希分桶时,用户体验会抖动。
- 熔断只拒绝/降级,不回滚版本;回滚必须显式操作流量和实例。
- 灰度不能替代测试和压测;它只是把故障影响面变小,不是发现所有问题。
讲清楚
蓝绿是整站换环境,金丝雀是单服务按比例放量,全链路灰度是按请求打标签,让一条请求在所有服务上都走灰度版本。全链路的关键是五个环节:网关染色、Header/线程/MQ 透传、每一跳按标签选实例、配置热生效、出问题时切回。任何一跳没有灰度实例就回退稳定版,标签继续往下传。回滚是流量和配置动作,Sentinel 熔断只是拒绝请求和降级,不是回滚。灰度还要防共享资源被灰度流量打爆,数据库只能做向下兼容的变更。