動態

詳情 返回 返回

上線別再“一刀切”!Gateway 做流量染色 + 灰度發佈,告別線上事故 - 動態 詳情

大家好,我是小富~

最近團隊迭代頻繁,連續幾周都在做新功能上線,從測試環境驗證到生產環境放量,全程謹小慎微沒出一次故障,主要是用好了 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,知道該怎麼説了吧!

user avatar xiaoniuhululu 頭像 monkeynik 頭像 ayuan01 頭像 AmbitionGarden 頭像 tech 頭像 u_16769727 頭像 u_11365552 頭像 u_15702012 頭像 jiangyi 頭像 febobo 頭像 shumile_5f6954c414184 頭像 ahahan 頭像
點贊 53 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.