上线别再“一刀切”!Gateway 做流量染色 + 灰度发布,告别线上事故

本文涉及的产品
云原生网关 MSE Higress,422元/月
MSE Nacos/ZooKeeper 企业版试用,1600元额度,限量50份
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 小富分享:通过Spring Cloud Gateway实现流量染色与灰度发布,为请求打标签(如VIP用户),结合自定义路由规则,实现新版本平滑上线。支持按用户、比例、设备等维度灰度,提升发布安全性,助力高效可控的线上迭代。

大家好,我是小富~


最近团队迭代频繁,连续几周都在做新功能上线,从测试环境验证到生产环境放量,全程谨小慎微没出一次故障,主要是用好了 Spring Cloud Gateway 的 流量染色灰度发布


很多同学面试时被问用过 SpringCloud Gateway 吗?,只会说做限流鉴权,但这些都是网关的基础操作。要想出去吹,得说用网关解决线上新版本平稳上线的问题。比如今天要分享的流量染色 + 灰度发布,就是我司每次上线必用的核心方案。


什么是流量染色?为什么需要它?


很多同学听流量染色觉得抽象,其实一句话就能说透:给请求打身份标签,让链路中所有服务都能认得出它


比如我们做电商 APP 的新功能上线,想让 VIP 用户优先试用新版本,但普通用户继续用旧版本。怎么让订单、支付、库存这些下游服务知道当前请求是 VIP 用户的?


这时候就需要染色:请求进入网关时,判断用户身份是 VIP,就在请求头里加一个 X-Traffic-Tag: vip 的标识,这个过程就是流量染色


后续的订单服务拿到请求,看到 X-Traffic-Tag: vip,就走新版本的订单逻辑;支付服务看到这个标签,就用新的支付接口;甚至日志系统看到这个标签,都会单独记录VIP 新版本的日志,单独处理这部分请求。



流量染色的核心价值在于,打破所有流量无差别处理的局限。有了染色标签,灰度发布、A/B 测试、环境隔离(比如测试流量不进生产库)才能落地。



什么是灰度发布?


搞懂了流量染色,灰度发布就好理解了,基于染色标签,让部分流量走新版本,逐步验证稳定性


以前我们没做灰度时,上线都是一刀切:凌晨 2 点全量切换新版本,一旦出问题,所有用户都受影响,只能紧急回滚,既狼狈又容易丢数据。


现在用灰度发布,流程变成这样:



  • 上线前:只让内部测试账号(染色标签 X-Traffic-Tag: test)走新版本,验证功能没问题;


  • 上线初期:放 5% 的 VIP 用户(标签 vip)走新版本,观察日志和监控;


  • 上线中期:没问题就扩大到 30%、50% 的 VIP 用户;


  • 全量:确认稳定后,所有用户切换到新版本,灰度结束。



如果中间发现问题,比如 5% 的 VIP 用户反馈下单失败,直接把灰度规则关掉,所有流量切回旧版本,影响范围只有 5%,风险完全可控。


常见的灰度策略除了按用户标签,还有这些:



  • 按比例:10% 流量走新版本(比如用用户 ID 取模,ID 尾号为 0 的用户);


  • 按业务场景:只让 “新用户注册” 接口走新版本,老用户接口不变;


  • 按设备:iOS 用户先切新版本,Android 用户后续再切(避免不同设备适配问题同时爆发)。



实现流量染色 + 灰度发布


接下来是重点:基于 SpringCloud Gateway,如何写代码实现这两个功能?整个流程分几步:请求染色→灰度路由→效果验证,所有代码都是生产环境可直接复用的。


项目依赖


首先确保引入 Gateway 核心依赖(Spring Boot 2.7.x + Spring Cloud Alibaba 2021.0.4.0 版本):


<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

<!-- 用于服务发现(如果灰度路由到注册中心的服务) -->
<dependency>
   <groupId>com.alibaba.cloud</groupId>
   <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

第一步:实现流量染色


流量染色的核心是拦截所有请求,按规则打标签,用 Gateway 的 GlobalFilter 就能实现,所有请求都会经过这个过滤器,我们在这里判断用户身份,注入染色标签。


比如我们的规则是:



  • 如果请求参数里有 userType=vip,就给请求头加 X-Traffic-Tag: vip


  • 如果请求参数里有 userType=test,就加 X-Traffic-Tag: test


  • 其他请求默认加 X-Traffic-Tag: normal



代码实现:


@Configuration
public class TrafficDyeFilterConfig {

    // 定义全局过滤器,Order设为-1(确保比其他过滤器先执行,早染色早用)
    @Bean
    @Order(-1)
    public GlobalFilter trafficDyeFilter() {
        return (exchange, chain) -> {
            // 1. 获取请求中的用户标识(参数/Cookie)
            String userType = getUserTypeFromRequest(exchange);

            // 2. 根据用户类型设置染色标签
            String trafficTag = getTrafficTagByUserType(userType);

            // 3. 将染色标签注入请求头(传递给下游服务)
            exchange.getRequest().mutate()
                    .header("X-Traffic-Tag", trafficTag)
                    .build();
            
            // 4. 继续执行后续过滤器链
            return chain.filter(exchange);
        };
    }

    // 从请求参数或Cookie中获取用户类型

    private String getUserTypeFromRequest(ServerWebExchange exchange) {
        // 先查请求参数:比如 http://xxx?userType=vip
        List<String> userTypeParams = exchange.getRequest().getQueryParams().get("userType");
        if (userTypeParams != null && !userTypeParams.isEmpty()) {
            return userTypeParams.get(0);
        }
        // 默认返回normal
        return "normal";
    }

    // 根据用户类型映射染色标签
    private String getTrafficTagByUserType(String userType) {
        switch (userType) {
            case "vip":
                return "vip";
            case "test":
                return "test";
            default:
                return "normal";
        }
    }
}

关键说明



  • Order(-1) 很重要:确保染色过滤器比鉴权、限流过滤器先执行,避免后续逻辑拿不到染色标签;


  • 标签放在请求头 X-Traffic-Tag:下游服务(如订单服务)可以直接通过 request.getHeader("X-Traffic-Tag") 获取标签,做差异化处理;


  • 扩展性:如果需要更复杂的染色规则(比如按用户 ID 取模、按地区),直接在 getUserTypeFromRequest 里加逻辑即可。



第二步:实现灰度路由


染色后,下一步就是让不同标签的流量走不同版本的服务,这需要自定义 RoutePredicateFactory(路由断言工厂),判断请求的染色标签,匹配对应的服务路由。


比如我们的灰度规则是:



  • 染色标签为 viptest 的请求,路由到新版本服务(服务名 order-service-v2);


  • 其他请求(标签 normal),路由到旧版本服务(服务名 order-service-v1)。



自定义灰度断言工厂


// 自定义断言工厂,命名格式:XXXRoutePredicateFactory(固定后缀)
@Configuration
public class GrayRoutePredicateFactory extends AbstractRoutePredicateFactory<GrayRoutePredicateFactory.Config{

    // 染色标签的请求头名(和第一步的X-Traffic-Tag对应)
    private static final String TRAFFIC_TAG_HEADER = "X-Traffic-Tag";

    // 构造函数,指定配置类
    public GrayRoutePredicateFactory() {
        super(Config.class);
    }

    // 定义配置类:存储断言需要的参数(比如“需要匹配的染色标签”)
    @Validated
    public static class Config {
        // 允许的染色标签(比如["vip", "test"])
        @NotEmpty
        private List<String> allowTags;

        public List<String> getAllowTags() {
            return allowTags;
        }

        public void setAllowTags(List<String> allowTags) {
            this.allowTags = allowTags;
        }
    }

    // 读取配置参数的顺序(和application.yml中配置的顺序对应)
    @Override
    public List<String> shortcutFieldOrder() {
        return Collections.singletonList("allowTags");
    }

    // 核心逻辑:判断请求的染色标签是否在允许的列表中
    @Override
    public GatewayPredicate apply(Config config) {
        return new GatewayPredicate() {
            @Override
            public boolean test(ServerWebExchange exchange) {
                // 1. 获取请求头中的染色标签
                List<String> trafficTags = exchange.getRequest().getHeaders().get(TRAFFIC_TAG_HEADER);
                if (trafficTags == null || trafficTags.isEmpty()) {
                    return false// 没有标签,不匹配灰度路由
                }

                String trafficTag = trafficTags.get(0);
                // 2. 判断标签是否在允许的列表中(比如["vip", "test"])
                return config.getAllowTags().contains(trafficTag);
            }

            // 用于日志打印,方便调试
            @Override
            public String toString() {
                return "GrayRoutePredicate{allowTags=" + config.getAllowTags() + "}";
            }
        };
    }
}

配置网关路由


在配置文件 application.yml 中,用自定义的 GrayRoutePredicateFactory 配置路由规则,指定哪些标签的流量走哪个服务:


spring:
  cloud:
    gateway:
      routes:
        # 路由1:灰度流量(vip/test标签)→ 新版本服务(order-service-v2)
        - id: gray_route_v2
          uri: lb://order-service-v2 # 服务注册中心的新版本服务名
          predicates:
            # 自定义灰度断言:允许的标签是["vip", "test"]
            - name: GrayRoute
              args:
                allowTags[0]: vip
                allowTags[1]: test
            # 匹配订单接口的路径(比如 /api/order/
            - Path=/api/order/

          filters:
            # 路径重写(可选,根据实际业务调整)
            - RewritePath=/api/(?<segment>.), /${segment}

        # 路由2:普通流量(normal标签)→ 旧版本服务(order-service-v1)
        - id: normal_route_v1
          uri: lb://order-service-v1 # 旧版本服务名
          predicates:
            # 普通流量:不满足灰度断言,走这条路由
            - Path=/api/order/**
          filters:
            - RewritePath=/api/(?<segment>.
), /${segment}

关键说明



  • uri: lb://xxx:用 lb 协议表示从服务注册中心(如 Nacos)拉取服务实例,实现负载均衡;


  • 路由顺序:Gateway 按路由配置的顺序匹配,所以灰度路由(gray_route_v2)要放在普通路由前面,确保灰度流量优先匹配;


  • 扩展性:如果需要按比例灰度(比如 10% 流量走 v2),可以在 GrayRoutePredicateFactory 里加用户 ID 取模的逻辑,比如 userID % 10 == 0 才走 v2。



第三步:验证效果


代码和配置都做好后,验证是否生效,用 Postman 看是否路由到正确的服务:


请求地址:http://网关IP:网关端口/api/order/create?userType=vip,请求可以转发到 order-service-v2


线上环境要注意


刚才的代码是基础版,如果要在生产环境用还需要做 3 个优化,避免踩坑:


1. 染色标签的透传问题


如果下游服务还有多层调用(比如网关→订单服务→库存服务),要确保 X-Traffic-Tag 在整个调用链中传递,不能断。


如果你用 OpenFeign 做服务间调用,加一个 Feign 拦截器,自动把请求头中的 X-Traffic-Tag 传递下去:


@Component
public class FeignTrafficTagInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        // 从当前请求上下文获取染色标签(需要用ThreadLocal存储)
        String trafficTag = TrafficTagContextHolder.get();
        if (trafficTag != null) {
            template.header("X-Traffic-Tag", trafficTag);
        }
    }
}

如果用 Dubbo,在 Dubbo 过滤器中做类似的头传递。


2. 灰度规则的动态调整


如果每次调整灰度比例(比如从 5% 到 30%)都要改代码、重启网关,效率太低。


把灰度规则(比如允许的标签、比例)存到 Nacos 配置中心;网关监听 Nacos 配置变更,动态更新灰度断言的规则,不用重启服务。


3. 灰度失败的快速回滚


如果新版本出问题,需要立刻把所有流量切回旧版本。


在 Nacos 中加一个灰度开关(比如 gray.switch=false);


自定义断言工厂时,先判断开关是否开启:如果开关关闭,直接不匹配灰度路由,所有流量走旧版本。


说在最后


网关不只是转发工具,更是流量控制中心


很多同学把 SpringCloud Gateway 当成简单的转发工具,只用它做限流、鉴权,其实它的核心价值是控制流量的走向,通过流量染色给流量贴标签,通过灰度路由让流量走对路,这才是线上平稳上线的关键。


看到这说明你已经掌握了,所以下次面试再被问 Gateway,知道该怎么说了吧!


相关文章
|
Kubernetes JavaScript API
如何理解 Istio Ingress, 它与 API Gateway 有什么区别?东西流量?南北流量?
这三者都和流量治理密切相关,那么流量治理在过去和现在有什么区别呢?都是如何做的呢? 在学习istio的时候对流量管理加深了理解。什么是东西流量?什么是南北流量?
564 0
|
Java API Maven
SpringCloud Gateway - 集成 Sentinel 流量限流、熔断
SpringCloud Gateway - 集成 Sentinel 流量限流、熔断
6058 1
|
消息中间件 负载均衡 算法
Spring Cloud Gateway 网关如何实现灰度发布?
Spring Cloud Gateway 网关如何实现灰度发布?
|
Java Maven Sentinel
Spring Cloud GateWay 集成 Sentinel 分布式流量控制框架
本文基于 spring.boot2.1.11.RELEASE, spring.cloud Greenwich.SR4, Spring Cloud Alibaba2.1.1.RELEASE, Sentinel 1.7.1
5553 0
Spring Cloud GateWay 集成 Sentinel 分布式流量控制框架
|
Java 测试技术 应用服务中间件
Spring Cloud Gateway 扩展支持多版本控制及灰度发布
灰度发布 什么是灰度发布,概念请参考,我们来简单的通过下图来看下,通俗的讲: 为了保证服务升级过程的平滑过渡提高客户体验,会一部分用户 一部分用户递进更新,这样生产中会同时出现多个版本的客户端,为了保证多个版本客户端的可用需要对应的多个版本的服务端版本。
2911 0
|
2月前
|
负载均衡 监控 Java
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
本文详细介绍了 Spring Cloud Gateway 的核心功能与实践配置。首先讲解了网关模块的创建流程,包括依赖引入(gateway、nacos 服务发现、负载均衡)、端口与服务发现配置,以及路由规则的设置(需注意路径前缀重复与优先级 order)。接着深入解析路由断言,涵盖 After、Before、Path 等 12 种内置断言的参数、作用及配置示例,并说明了自定义断言的实现方法。随后重点阐述过滤器机制,区分路由过滤器(如 AddRequestHeader、RewritePath、RequestRateLimiter 等)与全局过滤器的作用范围与配置方式,提
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
|
19天前
|
缓存 JSON NoSQL
别再手写过滤器!SpringCloud Gateway 内置30 个,少写 80% 重复代码
小富分享Spring Cloud Gateway内置30+过滤器,涵盖请求、响应、路径、安全等场景,无需重复造轮子。通过配置实现Header处理、限流、重试、熔断等功能,提升网关开发效率,避免代码冗余。
192 1
|
4月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
293 0
|
5月前
|
缓存 监控 Java
说一说 SpringCloud Gateway 堆外内存溢出排查
我是小假 期待与你的下一次相遇 ~
681 5
|
5月前
|
Java API Nacos