上線別再“一刀切”!Gateway 做流量染色 + 灰度發(fā)布,告別線上事故
最近團(tuán)隊(duì)迭代頻繁,連續(xù)幾周都在做新功能上線,從測(cè)試環(huán)境驗(yàn)證到生產(chǎn)環(huán)境放量,全程謹(jǐn)小慎微沒(méi)出一次故障,主要是用好了 Spring Cloud Gateway 的 流量染色 和 灰度發(fā)布。
很多同學(xué)面試時(shí)被問(wèn)用過(guò) SpringCloud Gateway 嗎?,只會(huì)說(shuō)做限流、鑒權(quán),但這些都是網(wǎng)關(guān)的基礎(chǔ)操作。要想出去吹,得說(shuō)用網(wǎng)關(guān)解決線上新版本平穩(wěn)上線的問(wèn)題。比如今天要分享的流量染色 + 灰度發(fā)布,就是我司每次上線必用的核心方案。
什么是流量染色?為什么需要它?
很多同學(xué)聽(tīng)流量染色覺(jué)得抽象,其實(shí)一句話就能說(shuō)透:給請(qǐng)求打身份標(biāo)簽,讓鏈路中所有服務(wù)都能認(rèn)得出它。
比如我們做電商 APP 的新功能上線,想讓 VIP 用戶優(yōu)先試用新版本,但普通用戶繼續(xù)用舊版本。怎么讓訂單、支付、庫(kù)存這些下游服務(wù)知道當(dāng)前請(qǐng)求是 VIP 用戶的?
這時(shí)候就需要染色:請(qǐng)求進(jìn)入網(wǎng)關(guān)時(shí),判斷用戶身份是 VIP,就在請(qǐng)求頭里加一個(gè) X-Traffic-Tag: vip 的標(biāo)識(shí),這個(gè)過(guò)程就是流量染色。
后續(xù)的訂單服務(wù)拿到請(qǐng)求,看到 X-Traffic-Tag: vip,就走新版本的訂單邏輯;支付服務(wù)看到這個(gè)標(biāo)簽,就用新的支付接口;甚至日志系統(tǒng)看到這個(gè)標(biāo)簽,都會(huì)單獨(dú)記錄VIP 新版本的日志,單獨(dú)處理這部分請(qǐng)求。
流量染色的核心價(jià)值在于,打破所有流量無(wú)差別處理的局限。有了染色標(biāo)簽,灰度發(fā)布、A/B 測(cè)試、環(huán)境隔離(比如測(cè)試流量不進(jìn)生產(chǎn)庫(kù))才能落地。
什么是灰度發(fā)布?
搞懂了流量染色,灰度發(fā)布就好理解了,基于染色標(biāo)簽,讓部分流量走新版本,逐步驗(yàn)證穩(wěn)定性。
以前我們沒(méi)做灰度時(shí),上線都是一刀切:凌晨 2 點(diǎn)全量切換新版本,一旦出問(wèn)題,所有用戶都受影響,只能緊急回滾,既狼狽又容易丟數(shù)據(jù)。
現(xiàn)在用灰度發(fā)布,流程變成這樣:
- 上線前:只讓內(nèi)部測(cè)試賬號(hào)(染色標(biāo)簽 X-Traffic-Tag: test)走新版本,驗(yàn)證功能沒(méi)問(wèn)題;
- 上線初期:放 5% 的 VIP 用戶(標(biāo)簽 vip)走新版本,觀察日志和監(jiān)控;
- 上線中期:沒(méi)問(wèn)題就擴(kuò)大到 30%、50% 的 VIP 用戶;
- 全量:確認(rèn)穩(wěn)定后,所有用戶切換到新版本,灰度結(jié)束。
如果中間發(fā)現(xiàn)問(wèn)題,比如 5% 的 VIP 用戶反饋下單失敗,直接把灰度規(guī)則關(guān)掉,所有流量切回舊版本,影響范圍只有 5%,風(fēng)險(xiǎn)完全可控。
常見(jiàn)的灰度策略除了按用戶標(biāo)簽,還有這些:
- 按比例:10% 流量走新版本(比如用用戶 ID 取模,ID 尾號(hào)為 0 的用戶);
- 按業(yè)務(wù)場(chǎng)景:只讓 “新用戶注冊(cè)” 接口走新版本,老用戶接口不變;
- 按設(shè)備:iOS 用戶先切新版本,Android 用戶后續(xù)再切(避免不同設(shè)備適配問(wèn)題同時(shí)爆發(fā))。
實(shí)現(xiàn)流量染色 + 灰度發(fā)布
接下來(lái)是重點(diǎn):基于 SpringCloud Gateway,如何寫代碼實(shí)現(xiàn)這兩個(gè)功能?整個(gè)流程分幾步:請(qǐng)求染色→灰度路由→效果驗(yàn)證,所有代碼都是生產(chǎn)環(huán)境可直接復(fù)用的。
項(xiàng)目依賴
首先確保引入 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>
<!-- 用于服務(wù)發(fā)現(xiàn)(如果灰度路由到注冊(cè)中心的服務(wù)) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>第一步:實(shí)現(xiàn)流量染色
流量染色的核心是攔截所有請(qǐng)求,按規(guī)則打標(biāo)簽,用 Gateway 的 GlobalFilter 就能實(shí)現(xiàn),所有請(qǐng)求都會(huì)經(jīng)過(guò)這個(gè)過(guò)濾器,我們?cè)谶@里判斷用戶身份,注入染色標(biāo)簽。
比如我們的規(guī)則是:
- 如果請(qǐng)求參數(shù)里有 userType=vip,就給請(qǐng)求頭加 X-Traffic-Tag: vip;
- 如果請(qǐng)求參數(shù)里有 userType=test,就加 X-Traffic-Tag: test;
- 其他請(qǐng)求默認(rèn)加 X-Traffic-Tag: normal。
代碼實(shí)現(xiàn):
@Configuration
publicclass TrafficDyeFilterConfig {
// 定義全局過(guò)濾器,Order設(shè)為-1(確保比其他過(guò)濾器先執(zhí)行,早染色早用)
@Bean
@Order(-1)
public GlobalFilter trafficDyeFilter() {
return (exchange, chain) -> {
// 1. 獲取請(qǐng)求中的用戶標(biāo)識(shí)(參數(shù)/Cookie)
String userType = getUserTypeFromRequest(exchange);
// 2. 根據(jù)用戶類型設(shè)置染色標(biāo)簽
String trafficTag = getTrafficTagByUserType(userType);
// 3. 將染色標(biāo)簽注入請(qǐng)求頭(傳遞給下游服務(wù))
exchange.getRequest().mutate()
.header("X-Traffic-Tag", trafficTag)
.build();
// 4. 繼續(xù)執(zhí)行后續(xù)過(guò)濾器鏈
return chain.filter(exchange);
};
}
// 從請(qǐng)求參數(shù)或Cookie中獲取用戶類型
private String getUserTypeFromRequest(ServerWebExchange exchange) {
// 先查請(qǐng)求參數(shù):比如 http://xxx?userType=vip
List<String> userTypeParams = exchange.getRequest().getQueryParams().get("userType");
if (userTypeParams != null && !userTypeParams.isEmpty()) {
return userTypeParams.get(0);
}
// 默認(rèn)返回normal
return"normal";
}
// 根據(jù)用戶類型映射染色標(biāo)簽
private String getTrafficTagByUserType(String userType) {
switch (userType) {
case"vip":
return"vip";
case"test":
return"test";
default:
return"normal";
}
}
}關(guān)鍵說(shuō)明:
- Order(-1) 很重要:確保染色過(guò)濾器比鑒權(quán)、限流過(guò)濾器先執(zhí)行,避免后續(xù)邏輯拿不到染色標(biāo)簽;
- 標(biāo)簽放在請(qǐng)求頭 X-Traffic-Tag:下游服務(wù)(如訂單服務(wù))可以直接通過(guò) request.getHeader("X-Traffic-Tag") 獲取標(biāo)簽,做差異化處理;
- 擴(kuò)展性:如果需要更復(fù)雜的染色規(guī)則(比如按用戶 ID 取模、按地區(qū)),直接在 getUserTypeFromRequest 里加邏輯即可。
第二步:實(shí)現(xiàn)灰度路由
染色后,下一步就是讓不同標(biāo)簽的流量走不同版本的服務(wù),這需要自定義 RoutePredicateFactory(路由斷言工廠),判斷請(qǐng)求的染色標(biāo)簽,匹配對(duì)應(yīng)的服務(wù)路由。
比如我們的灰度規(guī)則是:
- 染色標(biāo)簽為 vip 或 test 的請(qǐng)求,路由到新版本服務(wù)(服務(wù)名 order-service-v2);
- 其他請(qǐng)求(標(biāo)簽 normal),路由到舊版本服務(wù)(服務(wù)名 order-service-v1)。
自定義灰度斷言工廠
// 自定義斷言工廠,命名格式:XXXRoutePredicateFactory(固定后綴)
@Configuration
publicclass GrayRoutePredicateFactory extends AbstractRoutePredicateFactory<GrayRoutePredicateFactory.Config> {
// 染色標(biāo)簽的請(qǐng)求頭名(和第一步的X-Traffic-Tag對(duì)應(yīng))
privatestaticfinal String TRAFFIC_TAG_HEADER = "X-Traffic-Tag";
// 構(gòu)造函數(shù),指定配置類
public GrayRoutePredicateFactory() {
super(Config.class);
}
// 定義配置類:存儲(chǔ)斷言需要的參數(shù)(比如“需要匹配的染色標(biāo)簽”)
@Validated
publicstaticclass Config {
// 允許的染色標(biāo)簽(比如["vip", "test"])
@NotEmpty
private List<String> allowTags;
public List<String> getAllowTags() {
return allowTags;
}
public void setAllowTags(List<String> allowTags) {
this.allowTags = allowTags;
}
}
// 讀取配置參數(shù)的順序(和application.yml中配置的順序?qū)?yīng))
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList("allowTags");
}
// 核心邏輯:判斷請(qǐng)求的染色標(biāo)簽是否在允許的列表中
@Override
public GatewayPredicate apply(Config config) {
returnnew GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
// 1. 獲取請(qǐng)求頭中的染色標(biāo)簽
List<String> trafficTags = exchange.getRequest().getHeaders().get(TRAFFIC_TAG_HEADER);
if (trafficTags == null || trafficTags.isEmpty()) {
returnfalse; // 沒(méi)有標(biāo)簽,不匹配灰度路由
}
String trafficTag = trafficTags.get(0);
// 2. 判斷標(biāo)簽是否在允許的列表中(比如["vip", "test"])
return config.getAllowTags().contains(trafficTag);
}
// 用于日志打印,方便調(diào)試
@Override
public String toString() {
return"GrayRoutePredicate{allowTags=" + config.getAllowTags() + "}";
}
};
}
}配置網(wǎng)關(guān)路由
在配置文件 application.yml 中,用自定義的 GrayRoutePredicateFactory 配置路由規(guī)則,指定哪些標(biāo)簽的流量走哪個(gè)服務(wù):
spring:
cloud:
gateway:
routes:
# 路由1:灰度流量(vip/test標(biāo)簽)→ 新版本服務(wù)(order-service-v2)
-id:gray_route_v2
uri:lb://order-service-v2# 服務(wù)注冊(cè)中心的新版本服務(wù)名
predicates:
# 自定義灰度斷言:允許的標(biāo)簽是["vip", "test"]
-name:GrayRoute
args:
allowTags[0]:vip
allowTags[1]:test
# 匹配訂單接口的路徑(比如 /api/order/**)
-Path=/api/order/**
filters:
# 路徑重寫(可選,根據(jù)實(shí)際業(yè)務(wù)調(diào)整)
-RewritePath=/api/(?<segment>.*),/$\{segment}
# 路由2:普通流量(normal標(biāo)簽)→ 舊版本服務(wù)(order-service-v1)
-id:normal_route_v1
uri:lb://order-service-v1# 舊版本服務(wù)名
predicates:
# 普通流量:不滿足灰度斷言,走這條路由
-Path=/api/order/**
filters:
-RewritePath=/api/(?<segment>.*),/$\{segment}關(guān)鍵說(shuō)明:
- uri: lb://xxx:用 lb 協(xié)議表示從服務(wù)注冊(cè)中心(如 Nacos)拉取服務(wù)實(shí)例,實(shí)現(xiàn)負(fù)載均衡;
- 路由順序:Gateway 按路由配置的順序匹配,所以灰度路由(gray_route_v2)要放在普通路由前面,確保灰度流量?jī)?yōu)先匹配;
- 擴(kuò)展性:如果需要按比例灰度(比如 10% 流量走 v2),可以在 GrayRoutePredicateFactory 里加用戶 ID 取模的邏輯,比如 userID % 10 == 0 才走 v2。
第三步:驗(yàn)證效果
代碼和配置都做好后,驗(yàn)證是否生效,用 Postman 看是否路由到正確的服務(wù):
請(qǐng)求地址:http://網(wǎng)關(guān)IP:網(wǎng)關(guān)端口/api/order/create?userType=vip,請(qǐng)求可以轉(zhuǎn)發(fā)到 order-service-v2
線上環(huán)境要注意
剛才的代碼是基礎(chǔ)版,如果要在生產(chǎn)環(huán)境用還需要做 3 個(gè)優(yōu)化,避免踩坑:
1. 染色標(biāo)簽的透?jìng)鲉?wèn)題
如果下游服務(wù)還有多層調(diào)用(比如網(wǎng)關(guān)→訂單服務(wù)→庫(kù)存服務(wù)),要確保 X-Traffic-Tag 在整個(gè)調(diào)用鏈中傳遞,不能斷。
如果你用 OpenFeign 做服務(wù)間調(diào)用,加一個(gè) Feign 攔截器,自動(dòng)把請(qǐng)求頭中的 X-Traffic-Tag 傳遞下去:
@Component
public class FeignTrafficTagInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 從當(dāng)前請(qǐng)求上下文獲取染色標(biāo)簽(需要用ThreadLocal存儲(chǔ))
String trafficTag = TrafficTagContextHolder.get();
if (trafficTag != null) {
template.header("X-Traffic-Tag", trafficTag);
}
}
}如果用 Dubbo,在 Dubbo 過(guò)濾器中做類似的頭傳遞。
2. 灰度規(guī)則的動(dòng)態(tài)調(diào)整
如果每次調(diào)整灰度比例(比如從 5% 到 30%)都要改代碼、重啟網(wǎng)關(guān),效率太低。
把灰度規(guī)則(比如允許的標(biāo)簽、比例)存到 Nacos 配置中心;網(wǎng)關(guān)監(jiān)聽(tīng) Nacos 配置變更,動(dòng)態(tài)更新灰度斷言的規(guī)則,不用重啟服務(wù)。
3. 灰度失敗的快速回滾
如果新版本出問(wèn)題,需要立刻把所有流量切回舊版本。
在 Nacos 中加一個(gè)灰度開(kāi)關(guān)(比如 gray.switch=false);
自定義斷言工廠時(shí),先判斷開(kāi)關(guān)是否開(kāi)啟:如果開(kāi)關(guān)關(guān)閉,直接不匹配灰度路由,所有流量走舊版本。
說(shuō)在最后
網(wǎng)關(guān)不只是轉(zhuǎn)發(fā)工具,更是流量控制中心。
很多同學(xué)把 SpringCloud Gateway 當(dāng)成簡(jiǎn)單的轉(zhuǎn)發(fā)工具,只用它做限流、鑒權(quán),其實(shí)它的核心價(jià)值是控制流量的走向,通過(guò)流量染色給流量貼標(biāo)簽,通過(guò)灰度路由讓流量走對(duì)路,這才是線上平穩(wěn)上線的關(guān)鍵。
看到這說(shuō)明你已經(jīng)掌握了,所以下次面試再被問(wèn) Gateway,知道該怎么說(shuō)了吧!




























