跳到正文

蓝绿灰度与多级路由 ​

一句话:蓝绿是整站换环境,金丝雀是按比例放量,全链路灰度是按请求打标签并在每一跳独立选版本;回滚靠流量和配置切回,不靠熔断。

适用:Spring Cloud Gateway、Consul 1.16+、Apollo 2.x、Sentinel 1.8+;Kubernetes 侧的发布机制见 03-灰度发布与交付平台。

为什么需要 ​

新版本不能一次性推给所有用户。出问题时,希望影响面可控、回滚足够快;验证时,希望真实流量能走到新版本,而不是只看测试环境。

蓝绿、金丝雀、全链路灰度解决的是不同粒度的问题:整站切换、单服务放量、跨多个服务的联动变更。混用概念会导致「以为在灰度,其实一半流量已经走了新链路」。

核心机制 ​

三种发布策略 ​

蓝绿金丝雀全链路灰度
粒度环境级,整站切换服务级,按比例请求级,按标签
新旧共存通常不共存共存共存
回滚切回旧环境比例拉到 0关闭染色或摘除灰度实例
适用核心系统整体切换单服务逐步放量多服务联动变更

蓝绿需要双倍资源,但切换和回滚最干脆;金丝雀资源成本低,但只控制一个服务的比例;全链路灰度最灵活,也最依赖每一跳都支持标签透传。

请求级灰度的五个环节 ​

一条请求走完灰度,要经过:染色 → 透传 → 路由 → 生效 → 回滚。

  1. 染色:网关按规则给请求打 x-gray-tag。规则可以来自白名单、用户哈希、Header 或随机比例。
  2. 透传:HTTP Header、线程上下文、MQ 消息属性都要带标签。线程池必须用 TTL(TransmittableThreadLocal)包装,否则异步线程拿不到标签。
  3. 路由:每一跳的 LoadBalancer 读取注册中心的灰度元数据,按标签选实例;没有匹配实例时回退稳定版。
  4. 生效:规则热更新,不重启;实例上下线由注册中心感知。
  5. 回滚:关闭染色、摘除灰度实例、切回稳定版。回滚是流量动作,不是代码动作。

五种灰度场景 ​

场景标签灰度实例说明
单服务v2-bB只升级 B
相邻联动v2-abA、BA 和 B 接口一起变
跨跳联动v2-acA、C中间 B 保持稳定
全链路v2A、B、C大版本整体验证
链尾v2-dD只改下游末端

核心规则只有一条:请求的标签是否属于实例注册的灰度标签集合。一个服务的灰度实例缺失时,回退稳定版,标签继续往下传;否则一个中间服务没升级,整条灰度链就断了。

标签透传的三个通道 ​

  • 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/123

LoadBalancer 选择逻辑(伪代码):

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 熔断只是拒绝请求和降级,不是回滚。灰度还要防共享资源被灰度流量打爆,数据库只能做向下兼容的变更。

把一门技术讲透到既懂也会用