大家好,我是小富~
最近團隊迭代頻繁,連續幾周都在做新功能上線,從測試環境驗證到生產環境放量,全程謹小慎微沒出一次故障,主要是用好了 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(路由斷言工廠),判斷請求的染色標籤,匹配對應的服務路由。
比如我們的灰度規則是:
- 染色標籤為
vip或test的請求,路由到新版本服務(服務名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,知道該怎麼説了吧!