小Cの已经记不起来的博客

微服务架构下 灰度发布 的设计与权衡

前阵子给订单服务做灰度,本来以为在网关配个权重就完事了,结果链路一深,问题全冒出来了。A服务灰度了,B服务没灰度,C服务又调了个旧版本,最后用户看到的页面一半新一半旧,日志里连这个请求到底走了哪个版本都查不出来。所以这篇不聊什么“灰度发布最佳实践”,就聊聊我踩过的坑和最后怎么权衡的,没准儿你们团队也正卡在某个点上。

入口灰度最容易,也最容易给人错觉

最开始我就是在网关上加了个按比例分流,感觉灰度这玩意儿不过如此。比如 Spring Cloud Gateway 里按请求头或者用户 ID 做路由,配置大概长这样:

spring:
  cloud:
    gateway:
      routes:
        - id: order-gray
          uri: lb://order-service
          predicates:
            - Header=X-Gray, v2
          filters:
            - StripPrefix=1

再配合注册中心里给实例打 version=v2 标签,权重想调多少调多少。入口灰度确实简单,但它只覆盖了第一跳。后面的服务调用如果还是轮询,那就等于把灰度流量又撒回了全量池子里,用户刷新两次可能一次新一次旧,体验非常割裂。我一开始就吃了这个亏,以为网关配完就收工,结果测试同学一句“为什么同一个账号一会儿能看见新按钮一会儿看不见”把我问住了。入口灰度只能算个开始,别把它当成全链路灰度。

链路染色才是微服务灰度的分水岭

微服务下真正麻烦的是把“这个请求属于灰度”这个信息一路传下去。我一般是在入口解析出灰度标识,塞进请求头,比如 X-Gray: v2,然后通过 RPC 拦截器透传到下游。Spring Cloud OpenFeign 里大概就是加一个 RequestInterceptor,把上下文里的标识写进 header:

@Bean
public RequestInterceptor grayInterceptor() {
    return template -> {
        String gray = GrayContext.get();
        if (gray != null) {
            template.header("X-Gray", gray);
        }
    };
}

这里有个坑,GrayContext 如果只用普通 ThreadLocal,一进线程池就丢了,异步任务、@Async、定时任务全都会断链。我后来换成了 TransmittableThreadLocal,并且在线程池提交任务的时候包一层,不然日志里一会儿有灰度标识一会儿没有,排查起来很痛苦。消息队列也一样,生产者在消息 header 里带上灰度标识,消费者根据标识决定走新逻辑还是旧逻辑,不然灰度流量进了 MQ 就像泥牛入海,你根本不知道它被谁消费了。全链路灰度不是银弹,但链路染色不做,后面的监控和回滚基本靠猜。

数据兼容比流量切换更难

流量怎么切其实是相对好解决的,难的是新旧版本同时跑的时候数据怎么办。我见过最离谱的是灰度版本给表加了个非空字段,结果旧版本写入直接报错,回滚都来不及。所以灰度期间数据库 schema 必须向前兼容,一般是先加字段、允许为空,新版本双写或者写默认值,等旧版本流量彻底下线再收紧约束。缓存也一样,key 里最好带上版本,不然新版本写进去的缓存被旧版本读出来,反序列化直接炸。接口返回值加字段还好,删字段或者改语义就要命了,客户端可不管你是不是灰度。我的建议是灰度发布前先把“数据能不能共存”这件事想清楚,别等流量切了 20% 才发现旧代码读不了新数据,那时候回滚就不是调个权重那么简单了。

配置中心和注册中心别只当摆设

灰度规则我一般会放到配置中心里,权重、白名单、开关都做成动态的,不然每次调 1% 都要重新发版,灰度本身就成了笑话。Nacos 或者 Apollo 里放一个灰度配置,大概这样:

gray:
  enabled: true
  version: v2
  percent: 5
  white-list:
    - userId: 10001
    - userId: 10002

但配置一多,问题也跟着来:没人知道当前到底哪个开关生效,测试环境一套、生产环境一套,最后排查问题先花半小时看配置。我后来强制要求灰度配置收敛到一个 namespace,开关命名带上业务和版本,删掉半年没人动的僵尸配置。注册中心的实例标签也别乱打,version=v2 和 gray=true 最好统一语义,不然网关按一个标签路由,服务间按另一个标签找实例,不出问题才怪。

回滚不是把权重调回去就完了

很多人觉得灰度出问题把权重调回 0 就完事了,实际上还要看会话、缓存、消息和已经写入的数据。用户可能已经拿着新版本的 token,缓存里可能已经是新结构,MQ 里可能还压着一批灰度消息。所以灰度批次要小,观察窗口要够,监控得能按版本维度看错误率、延迟和业务指标,日志里至少带上 traceId 和 version,不然你连“哪个版本坏了”都说不清。我现在的习惯是第一批只放内部用户和白名单,观察至少半小时,没问题再放 5%,然后 20%,最后全量。回滚预案提前写好,别等出事再翻文档。

最后说权衡:别为了 10% 流量把系统搞成迷宫

全链路灰度很香,但成本也摆在那儿:网关、RPC、MQ、缓存、数据库、监控,每一层都要支持版本识别,团队得有一两个人专门维护这套东西,不然规则一多就没人敢动。小团队如果服务不多,网关按 header 分流加配置中心开关,再配合日志里打版本,其实就能覆盖大部分场景,没必要硬上 Service Mesh。灰度粒度也不是越细越好,按用户 ID 哈希、按白名单、按实例权重,够用就行。说到底,灰度发布是在“发布风险”和“系统复杂度”之间找平衡,不是把链路染得越花越好。不出问题的话,灰度就算发完了;出问题的话,至少知道该从哪回滚,而不是一群人围着日志干瞪眼。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放