☰
Spring Cloud全链路流量染色:灰度发布标记透传方案详解
2026/10/14 9:18:18 网站建设 项目流程

做微服务灰度发布,很多人一开始以为难点在“流量怎么分”,做起来才发现真正的难点在“标记怎么不丢”。我见过一个特别典型的事故:新版本接口已经灰度给了一部分用户,请求也确实打到了灰度实例上,但这条请求内部通过 OpenFeign 继续调用下游服务时,灰度标记丢失了,下游服务把头一跳当成了普通稳定流量,直接走了稳定逻辑、写了稳定环境的表。最后线上数据对不上,只能靠人工对账一点一点捞回来。这类问题的根子只有一个:你只做了入口灰度,没有做全链路流量染色。

Spring Cloud 体系里做灰度发布,真正要打通的不只是一条网关路由规则,而是一整条从入口到服务、从服务到数据、再从异步线程到消息队列的“染色传递链”。今天这篇就把全链路流量染色的完整设计方案拆开讲清楚,包含顶层设计思路、Gateway 染色、Feign/RestTemplate/线程池/MQ 的透传细节、负载均衡决策、数据面隔离,以及一堆只有真正踩过坑才会知道的注意事项。

1. 灰度发布卡在哪一步:标记断链才是隐形杀手

1.1 大多数团队只完成了“入口灰度”

不少团队做灰度发布,第一步就是改网关:按用户 ID、按百分比、按特定 Header,把一部分流量分到灰度实例上去。改造完网关,测试几条链路,看到灰度实例有流量进来了,就以为自己完成了灰度发布。这是最大的误判。

入口灰度只解决了“第一跳”的问题。真实业务链路里,一次用户请求往往会经过三层、四层甚至更多层的服务调用。网关把流量交给用户服务之后,用户服务要调订单服务,订单服务要调库存服务,每一层都可能发起新的 HTTP 调用、异步任务、消息发送。如果只有网关知道“这是一条灰度请求”,后面的服务不知道,那灰度就是不完整的。

以我之前排查过的一个场景为例:支付回调链路灰度到了新逻辑,但回调处理服务通过 RestTemplate 调用另一个老接口时,老接口没有感知到灰度标记,直接执行了旧逻辑。那个旧逻辑和灰度新逻辑在数据写入上存在冲突,最后出现了重复扣减的问题。这不是路由策略的问题,是标记没有随调用传递下去的问题。

1.2 断链之后会发生什么

标记断链的后果,通常不是“功能完全不可用”,而是“部分数据、部分行为不一致”,这类问题最难排查。

举几个我实际见过的断链表现:

  • 灰度用户请求同时打到灰度实例和稳定实例,同一个用户两次操作看到的页面状态不一样,用户以为是系统出 Bug。
  • 灰度流量写入的数据进了稳定环境的数据库表,灰度功能下线后,这部分数据永远留在稳定环境里,没有任何标记可以区分。
  • 异步线程池里的任务丢了灰度标记,同一个请求主线程走灰度逻辑,子线程却走稳定逻辑,导致主数据与衍生数据对不上。
  • 灰度实例上产生的业务消息发送到了稳定的 MQ Topic,下游消费方完全不知道这条消息是灰度消息,照常处理并落库。

它本质上是一致性问题:一条业务链路不同节点对“当前请求是否灰度”的判断结果不一样。只要有一个环节判断不一致,整条链路的结果就不可信。

1.3 为什么不能让每个服务自己判断灰度

有人会问:既然下游服务也需要知道要不要走灰度,那每个服务自己配一套规则不就行了?比如网关按用户取模,用户服务自己也按用户取模,订单服务也按用户取模,大家都用同一个公式,不就能形成一致的灰度判断了?

理论上是能,但实际工程里基本行不通。

第一个问题是规则难以同步。你很难保证用户服务、订单服务、库存服务同时上线同一个版本的灰度规则;规则更新时,灰度中的请求可能在几个服务里走到灰度版本,在另一个服务里却因为规则配置还没刷新而走到稳定版本。第二个问题是多灰度版本并存时会彻底失控。网关把某用户分到灰度 A,用户服务按自己的规则把他分到灰度 B,订单服务可能认为他是稳定流量,整个链路完全错乱。

全链路流量染色的核心思想,就是把这个判断过程收敛到一个地方:只有入口做灰度判定,判定结果以标记的形式附着在请求上,一路透传下去,下游服务不重新判断,只看标记、认标记。一条链路,一个标记,全局一致。

2. 流量染色的顶层设计:一条链路只认一个标记

2.1 标记长什么样:Header 设计与值域规划

流量染色最核心的载体是请求 Header。我用的是X-Gray-Tag,这个 Header 全链路统一命名,禁止任何服务私自改名。值域建议统一规划,不要把标记当成一个自由文本随意填。

一个比较实用的取值方式是“版本号即标记”:

值含义使用场景
stable稳定流量所有未被灰度规则命中的请求,兜底必须打这个值
gray-v1第一批灰度版本新功能小流量验证
gray-v2第二批灰度版本在 v1 基础上进行 A/B 验证

这里有个容易被忽略的点:stable必须显式打上,不能留空。因为下游服务判断时统一按标记值处理,空值容易产生歧义,也影响日志排查。

如果后期要支持更复杂的规则联动,可以再加一个辅助 HeaderX-Gray-Rule-Version,用来记录“当前灰度是由哪一版规则算出来的”。这样当规则调整时,你可以在日志里看出来请求到底命中的是旧规则还是新规则,对排查灰度流量异常非常有用。

2.2 灰度判定从哪来:入口规则的优先级设计

流量染色的“判定动作”只发生在入口,但入口的判定规则往往不止一种。实际项目中,我会按下列优先级从高到低执行:

  1. 强制指定 Header:比如X-Gray-Force: gray-v1,用于测试人员手动指定走哪个灰度版本,最直接,也最容易排查。
  2. Cookie 或用户标识:登录态里的用户 ID,按用户维度打灰度标记,适合“同一用户前后操作一致”的场景。
  3. 百分比规则:按请求 IP、用户 ID 哈希取模,适合无登录态的匿名流量。
  4. 白名单/内部环境标记:特定测试账号、特定来源 IP 强制进入灰度环境。

规则优先级的核心原则是:能被精确指定的,优先于模糊的百分比规则。否则测试人员在验证灰度功能时,自己都极有可能被随机规则分流到稳定版本。

2.3 三段式模型:染色、透传、决策

整个全链路流量染色的落地,我习惯拆成三段来设计和实施:

  • 染色(Tagging):入口网关根据规则给请求打上X-Gray-Tag,这是唯一一次灰度判定。
  • 透传(Propagation):标记随调用链路一路向下传递,包括 HTTP 同步调用、异步线程、消息队列。透传要保证“标记不丢失、不被篡改”。
  • 决策(Decision):每个服务拿到标记后,根据标记选择对应的实例版本、数据源、缓存策略、业务逻辑分支。

分段设计的好处是每一段都可以独立测试。比如你可以先只做染色和透传,让日志里能完整看到标记贯穿全链路,再慢慢加上服务端的路由决策。如果一上来就所有环节一起改,出了问题很难定位是标记没传下去,还是决策逻辑写错了。

3. 入口网关染色:给第一批流量系上红绳

3.1 网关 Filter 的打标逻辑

网关层我推荐用 Spring Cloud Gateway,在全局过滤器里统一处理染色逻辑。核心代码思路如下:

@Component public class GrayTagGatewayFilter implements GlobalFilter, Ordered { public static final String GRAY_TAG_HEADER = "X-Gray-Tag"; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String grayTag = resolveGrayTag(request); if (!StringUtils.hasText(grayTag)) { grayTag = "stable"; } ServerHttpRequest mutatedRequest = request.mutate() .header(GRAY_TAG_HEADER, grayTag) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); } private String resolveGrayTag(ServerHttpRequest request) { // 1. 强制指定 Header,测试用 String forced = request.getHeaders().getFirst("X-Gray-Force"); if (StringUtils.hasText(forced)) { return forced; } // 2. 按用户 ID 取模 String userId = resolveUserId(request); if (StringUtils.hasText(userId)) { int hash = Math.abs(userId.hashCode() % 100); if (hash < 10) { return "gray-v1"; } } // 3. 百分比灰度 String clientIp = resolveClientIp(request); if (clientIp != null && Math.abs(clientIp.hashCode() % 100) < 5) { return "gray-v1"; } return "stable"; } @Override public int getOrder() { return -100; } }

这段代码有几个细节上的讲究。getOrder()必须比绝大多数业务过滤器靠前,保证后续所有过滤器拿到的请求都已经是“染色后”的。resolveGrayTag里多层判断的原因前面说过了:能精确匹配的规则先行,百分比规则兜底。

灰度比例调整时,不用发布网关代码,可以把规则配置放到配置中心里,Filter 动态读取。比例从 1% 调到 5%、10%、50%,只改配置即可,这是灰度发布落地的刚需能力。

3.2 别忽略的业务豁免清单

不是所有请求都需要灰度,网关染色时必须有豁免逻辑。这是一个非常容易在实战中忽略的细节。

需要强制走稳定版本的请求至少包括:

  • 健康检查接口:Spring Boot Actuator 的/actuator/health等探活请求,如果被灰度规则命中,而灰度实例启动较慢,可能被误判为不健康。
  • 登录、注册、支付回调等关键接口:这些链路往往涉及资金和数据强一致,灰度期间一般不建议直接参与小流量分流。
  • 对下游有幂等要求的回调通知:支付平台、短信服务商的回调如果被打上灰度标记,一旦灰度逻辑有 Bug,影响会被放大。
  • 静态资源和无状态接口:不需要灰度的请求,免得白白增加判定成本。

具体实现上,我一般会在resolveGrayTag里先判断request.getPath().value(),命中豁免路径直接返回stable。这一步放在强制 Header 判断之后、其他规则之前,既保证测试人员能强制压测灰度路径,又能让线上关键请求稳稳地走稳定版本。

3.3 从网关到下游的第一次跳转

网关染色完成后,请求会转发到下游服务。这里有一个必须验证的细节:Header 是否真正传递到了下游服务。

Spring Cloud Gateway 默认会透传请求 Header,但如果你用了RewritePath、自定义过滤器重写了请求对象,或者手动构造了转发地址,自定义 Header 可能会在某个环节被丢弃。我建议在染色完成后立即在下游服务的入口日志里打印X-Gray-Tag,确认标记已经跨过了网关到服务的“第一道桥”。

网关到下游这一跳往往是正常的,真正容易出问题的是下游服务再次发起调用时,标记开始丢失。这就是下一部分要重点解决的透传问题。

4. 链路透传的四个硬骨头:Feign、RestTemplate、线程池、MQ

4.1 Feign:拦截器传 Header 的工程细节

Spring Cloud 微服务之间调用最多的方式就是 OpenFeign。Feign 不会自动传递上一个请求的 Header,需要借助RequestInterceptor在构建请求时主动写入。

一个典型的实现如下:

@Component public class GrayTagFeignRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (requestAttributes instanceof ServletRequestAttributes) { HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); String grayTag = request.getHeader(GrayTagGatewayFilter.GRAY_TAG_HEADER); if (StringUtils.hasText(grayTag)) { template.header(GrayTagGatewayFilter.GRAY_TAG_HEADER, grayTag); } } } }

如果你给每个 Feign Client 单独配了configuration,记得确认这个拦截器被所有需要传递标记的 Client 共享。我自己踩过一个坑:全局拦截器配好了,但某个特定 Feign Client 自己定义了 configuration,导致全局拦截器没有生效。后来我把拦截器注册成了全局 Bean,才彻底解决。

另外注意一个隐藏问题:RequestContextHolder.getRequestAttributes()在普通同步调用场景下能拿到当前请求上下文,但一旦请求进入了异步线程,这里拿到的就是 null。所以这个拦截器只能解决同步链路透传,异步链路要用后面 4.3 节的方法处理。

4.2 RestTemplate:别漏掉的同步调用拦截器

很多老项目还在用 RestTemplate 做服务间调用,它的透传方式和 Feign 类似,用ClientHttpRequestInterceptor搞定:

@Component public class GrayTagRestTemplateInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (requestAttributes instanceof ServletRequestAttributes) { HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest(); String grayTag = httpServletRequest.getHeader(GrayTagGatewayFilter.GRAY_TAG_HEADER); if (StringUtils.hasText(grayTag)) { request.getHeaders().set(GrayTagGatewayFilter.GRAY_TAG_HEADER, grayTag); } } return execution.execute(request, body); } }

业务代码里创建 RestTemplate 时,把这个拦截器加进去:

RestTemplate restTemplate = new RestTemplate(); restTemplate.getInterceptors().add(new GrayTagRestTemplateInterceptor());

如果你是通过RestTemplateBuilder自动装配的,建议在项目启动时统一配置一次,避免散落在各个业务代码里。全链路透传最忌讳“每个调用方自己记得加”,人总有记不住的时候,统一在基础设施里保证才能形成惯性约束。

4.3 异步线程池丢失标记:TaskDecorator 的标准解法

这是整个透传链路里最容易翻车的一环,也是我见过线上事故最多的地方。原因很朴素:@Async线程池和自建线程池里的子线程,拿不到主线程的ThreadLocal数据。RequestContextHolder本身也是用 ThreadLocal 实现的,所以主线程里的灰度标记天然传不到子线程。

标准解法是用TaskDecorator在任务提交时捕获主线程上下文,任务真正执行前恢复上下文,执行完再清理。

@Bean("grayTagAwareTaskExecutor") public Executor grayTagAwareTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(16); executor.setQueueCapacity(100); executor.setTaskDecorator(runnable -> { RequestAttributes context = RequestContextHolder.getRequestAttributes(); String grayTag = GrayTagContextHolder.getGrayTag(); return () -> { try { if (context != null) { RequestContextHolder.setRequestAttributes(context); } if (StringUtils.hasText(grayTag)) { GrayTagContextHolder.setGrayTag(grayTag); } runnable.run(); } finally { if (context != null) { RequestContextHolder.resetRequestAttributes(); } GrayTagContextHolder.clear(); } }; }); return executor; }

这里需要额外维护一个GrayTagContextHolder,用来在纯异步环境下保存灰度标记,不依赖 Servlet 请求上下文。因为异步子线程里RequestContextHolder本身可能是 null,只有把灰度标记单独放到一个自定义 ThreadLocal 容器里,才能保证后续再次调用 Feign 或 RestTemplate 时,拦截器能够读取到正确的标记信号。

使用@Async("grayTagAwareTaskExecutor")显式指定这个线程池,比全局替换默认线程池更可控,避免影响其他异步任务的既有行为。

4.4 MQ 链路:生产端打标、消费端取标

消息队列里的链路透传是另一个容易被遗忘的角落。HTTP 调用的标记传递靠 Header,消息链路就要靠消息头的属性字段。

生产端发送消息时,把当前线程的灰度标记写入消息属性:

Message message = MessageBuilder.withPayload(payload) .setHeader("X-Gray-Tag", GrayTagContextHolder.getGrayTag()) .build();

消费端接收消息后,第一步不是解析业务数据,而是把消息头里的灰度标记先捞出来,放入当前消费线程的上下文:

String grayTag = (String) message.getHeaders().get("X-Gray-Tag"); GrayTagContextHolder.setGrayTag(grayTag); try { // 业务处理 } finally { GrayTagContextHolder.clear(); }

为什么要强调“第一步”和“finally 清理”?因为消费线程如果处理完不清理,ThreadLocal 里的灰度标记会残留在线程池中的线程上,下一条消息可能拿到上一条消息的错误标记。这个问题不仔细排查很难发现,一旦发现通常已经造成了数据混乱。

更稳妥的方案是:如果灰度消息和稳定消息在业务处理上差异足够大,可以考虑拆分 Topic。灰度消息发到独立的gray-order-topic,消费端代码天然隔离,不需要在业务逻辑里反复判断标记。缺点是运维成本和 Topic 数量会上升,适合灰度功能差异特别大的场景,常规小迭代用消息头标记就够了。

5. 服务端决策:标记到了,实例怎么选

5.1 注册中心元数据:给灰度实例打版本标签

标记从入口一路传到了服务节点,接下来服务端要根据标记决定把请求路由到哪个实例。这里的关键前提是:实例自己必须先“自报家门”。如果所有实例都长一个样,负载均衡器无法区分稳定和灰度。

在 Spring Cloud 体系里,最通用的办法是给注册中心的服务实例元数据加上版本信息。以 Nacos 为例:

spring: application: name: order-service cloud: nacos: discovery: metadata: version: gray-v1

稳定版本实例的元数据里写version: stable,灰度版本实例写version: gray-v1,如果同时存在多批灰度,可以继续加gray-v2。这样注册中心里的服务列表就带上了版本标签,负载均衡器可以根据标签精确定位。

Eureka 也是同样的思路,通过eureka.instance.metadata-map.version=gray-v1配置。这只是载体差异,核心模型完全一致:实例元数据 + 请求标记匹配。

5.2 自定义负载均衡:灰度优先、稳定兜底

有了实例版本标签,下一步就是重写负载均衡逻辑。现在 Spring Cloud 体系推荐用 Spring Cloud LoadBalancer,以前基于 Ribbon 的那套已经逐步进入维护期了。自定义核心逻辑大致是这样:

private List<ServiceInstance> filterByGrayTag(List<ServiceInstance> instances, String grayTag) { if (StringUtils.hasText(grayTag) && !"stable".equals(grayTag)) { List<ServiceInstance> grayInstances = instances.stream() .filter(instance -> grayTag.equals(instance.getMetadata().get("version"))) .collect(Collectors.toList()); if (!grayInstances.isEmpty()) { return grayInstances; } // 灰度实例不存在,回退稳定版本 } return instances.stream() .filter(instance -> "stable".equals(instance.getMetadata().get("version"))) .collect(Collectors.toList()); }

这个逻辑里最关键的是“灰度实例不存在就回退稳定”。灰度发布过程中,灰度实例还在滚动部署,某个瞬间可能注册中心只有稳定实例存在。如果这时盲目选择灰度实例,请求会全部报错。加一个兜底,灰度标记打上了但找不到灰度实例时,请求自然落到稳定版本上,功能不受影响,只是这次的灰度体验降级了。

另外,灰度流量只能打给灰度实例,稳定流量绝对不能打给灰度实例。否则稳定版本的代码还没准备好,被灰度实例的新逻辑处理,后果不可控。所以在过滤时,“稳定流量只选 stable 实例”这个规则要写进公共配置,所有服务统一使用。

5.3 决策失败时的降级路径

再想深一层:如果灰度标记传到了,灰度实例也在,但灰度实例恰好发生了故障或正在重启,怎么办?

我见过一些项目在负载均衡器里把“找不到灰度实例”当成异常处理,直接把请求返回 500。这是非常糟糕的做法。正确思路是降级,也就是按“灰度优先、稳定兜底”的规则继续路由。

降级之外,还需要把决策结果暴露出来。日志里要明确记录:这条带gray-v1标记的请求,最终因为灰度实例不可用而路由到了稳定实例。这样你在灰度期间查看监控时,能看到一个额外指标:灰度流量降级率。如果降级率长期偏高,说明灰度实例规模不够或者健康检查有问题,需要优先处理基础设施,而不是继续加快灰度推进。

6. 数据面染色:接口灰度只是开始,数据隔离才是收官

6.1 数据库读写路由

接口层面的灰度做到位之后,紧接着的问题是:灰度逻辑产生的数据,到底写到哪里?

如果只是把请求路由到灰度实例,但实例读写的是同一个数据库、同一张表、同一个缓存,那灰度就只灰度了“代码逻辑”,没有灰度“数据存储”。几个用户的数据混进稳定数据里,测试和业务都很难受。

数据库层面的染色,业界常用的有几种方案:

  • 独立灰度库:单独建一套灰度环境和对应的库,灰度流量进入独立 schema,稳定流量完全不被触碰。这是物理隔离,最干净,适合灰度功能涉及大批量数据变更的场景。缺点是灰度环境和正式环境的切换要非常谨慎。
  • 共享库 + 版本字段:同一张表里增加一个gray_tag字段,灰度写入的数据打上灰度标记。查询时按标记过滤。这种方式适合表结构变动不大、灰度数据量可控的场景。
  • 动态数据源路由:结合 ThreadLocal 中的灰度标记,在 DAO 层切换数据源。灰度标记是gray-v1时连接到灰度数据源,否则连接稳定数据源。

我个人在常规迭代中更推荐“独立灰度库 + 稳定库双写”的方案。灰度流量在灰度库里读写自由,稳定流量毫无感知,灰度结束后把灰度库中需要保留的数据回放或者重新迁移到稳定库即可。但要注意双写往往伴随数据一致性问题,如果没有完善的补偿机制,宁可让灰度库单独承担,也不要一开始就搞复杂的双写。

6.2 缓存 Key 版本隔离

数据库隔离做了,缓存也不能漏。最直接的问题是:如果稳定版本和灰度版本的 Key 完全一样,灰度数据会被写进稳定版本的缓存里,影响稳定流量;反之稳定数据也会污染灰度验证结果。

我的做法是给缓存 Key 加版本前缀。

稳定版本的关键字:order:detail:stable:10001。

灰度版本的关键字:order:detail:gray-v1:10001。

加前缀会带来一点空间冗余,但换来的是灰度期间缓存完全隔离,任何一方都不会被对方污染。灰度结束后,把灰度 Key 清理掉即可,不会影响稳定缓存。

还有一个更隐蔽的坑:灰度版本使用了新的缓存结构,但灰度结束后忘了清灰度 Key,导致老数据残留。所以灰度切换结束后、稳定版本读完灰度遗留数据之前,要加一个缓存清理步骤,把灰度前缀的 Key 批量删除。

6.3 用日志 MDC 验证整条链路

说了这么多,怎么验证整条链路的染色真的贯穿起来了?最快的方法是日志。把灰度标记塞进日志的 MDC,全链路每一条日志都能带上grayTag字段,排查问题的时候能直接看清这条请求属于哪个版本。

网关 Filter 里和下游服务的 AOP 里各打一次 MDC:

MDC.put("grayTag", grayTag);

日志配置里输出这个字段:

%X{grayTag} %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n

日志里出现[gray-v1]前缀的,就是灰度请求。全链路验证时,我用一条测试请求从网关打进去,一路跟日志,看每一跳服务是不是都带着同样的标记。如果某一跳日志里没有grayTag字段,那就说明这一跳的透传断了,优先排查这一段的拦截器或者线程池装饰器。

这套验证方法在灰度发布实践里价值极大。它能在灰度上线之前就把“链路断点”找出来,而不是等到灰度用户反馈异常才去翻日志。

7. 落地中高频翻车点与自检清单

7.1 四个典型的翻车现场

第一个翻车点:Feign 自定义 configuration 覆盖了全局拦截器。某个调用链路的 Feign Client 自己配了自定义配置,全局的请求拦截器没有生效,灰度标记在“看似已经配好了”的情况下依然丢失。排查了很久才发现是 configuration 覆盖导致。

第二个翻车点:异步线程池没有统一的 TaskDecorator。团队里有人用自建ExecutorService发起了异步任务,没有走统一线程池,灰度标记在异步线程里拿不到,异步任务把灰度流量当稳定流量处理,产生了一堆脏数据。

第三个翻车点:自定义 Header 被误删。网关后面加了个日志记录过滤器,顺手清掉了一部分请求 Header,自定义的X-Gray-Tag正好在里面。功能上线时入口日志正常,下游服务入口日志里标记却直接消失了。

第四个翻车点:灰度实例稳定实例混在同一个负载均衡规则里。稳定流量没有强制过滤version=stable的实例,结果负载均衡把稳定流量随机打到了灰度实例上,灰度功能看到了一堆不该出现的稳定数据,稳定性评估完全失真。

这些翻车点有个共同特点:单个环节看起来都对,但全链路一起跑就乱。所以灰度发布上线前,建议按下面的清单逐项检查。

7.2 灰度上线的检查清单

我每次做灰度发布前,都会把下面这份清单完整过一遍,分享出来给需要的朋友参考:

  • 入口 Filter 是否对无标记流量强制打上stable,灰度标记值是否按规范命名
  • 健康检查、登录、支付回调等豁免接口是否排除在灰度规则之外
  • Feign 全局拦截器是否对所有需要灰度的 Client 生效,是否有局部 configuration 覆盖
  • RestTemplate 是否统一注入了灰度拦截器,有没有漏掉的调用出口
  • 异步线程池是否都走 TaskDecorator 包装,子线程执行后是否清理上下文
  • MQ 生产端是否写入灰度标记,消费端是否在处理业务前恢复标记、结束后清理标记
  • 注册中心实例元数据是否正确打上version标签,稳定实例是否明确标记为stable
  • 负载均衡器是否实现灰度优先、稳定兜底,稳定流量是否强制只走稳定实例
  • 数据库读写的灰度隔离方案是否明确,缓存 Key 是否有版本前缀
  • 日志 MDC 是否已经带上灰度标记字段,灰度验证请求能否全程跟踪

这份清单看起来长,但每一条背后都有真实事故支撑。灰度发布本身不难,难的是把“标记贯穿所有环节”这种细节做到滴水不漏。如果图省事跳过一两项,上线后排查问题的成本往往远高于提前检查的成本。

如果非要说一个最有价值的落地经验,我会说:先让日志里的灰度标记完整跑通一条链路,再谈流量路由。标记都穿不透,路由做得再花哨也是空转。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询