微服务架构下 灰度发布 的设计与权衡
前阵子给订单服务做灰度,本来以为在网关配个权重就完事了,结果链路一深,问题全冒出来了。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 哈希、按白名单、按实例权重,够用就行。说到底,灰度发布是在“发布风险”和“系统复杂度”之间找平衡,不是把链路染得越花越好。不出问题的话,灰度就算发完了;出问题的话,至少知道该从哪回滚,而不是一群人围着日志干瞪眼。
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。