實際生産中如有需求變更,并不會直接更新線上服務,最通常的做法便是:切出線上的小部分流量進行體驗測試,經過測試後無問題則全面的上線。
這樣做的好處也是非常明顯,一旦出現了BUG,能夠保證大部分的用戶端正常使用。
要實作這種平滑過渡的方式就需要用到本篇文章介紹到的全鍊路灰階釋出。
什麼是灰階釋出?
灰階釋出(又名金絲雀釋出)是指在黑與白之間,能夠平滑過渡的一種釋出方式。在其上可以進行A/B testing,即讓一部分使用者繼續用産品特性A,一部分使用者開始用産品特性B,如果使用者對B沒有什麼反對意見,那麼逐漸擴大範圍,把所有使用者都遷移到B上面來。灰階釋出可以保證整體系統的穩定,在初始灰階的時候就可以發現、調整問題,以保證其影響度。
為什麼是全鍊路灰階釋出?
在陳某前面一篇文章有介紹到網關的灰階釋出實作,僅僅是實作了網關路由轉發的灰階釋出,如下圖:
如上圖,網關灰階釋出實作的是網關通過灰階标記路由到文章服務B(灰階服務),至于從文章服務B到評論服務是通過openFeign内部調用的,預設無法實作灰階标記grayTag的透傳,是以文章服務B最終調用的是評論服務A,并不是評論服務B。
全鍊路灰階釋出需要實作的是:
- 網關通過灰階标記将部分流量轉發給文章服務B
- 文章服務B能夠實作灰階标記grayTag的透傳,最終調用評論服務B
經過以上分析,全鍊路灰階釋出需要實作兩個點:
- 網關路由轉發實作灰階釋出
- 服務内部通過openFeign調用實作灰階釋出(透傳灰階标記grayTag)。
下面将以陳某的《Spring Cloud Alibaba實戰》專欄中的服務為例進行灰階釋出配置。
網關層的灰階路由轉發
本篇文章将使用Ribbon+Spring Cloud Gateway 進行改造負載均衡政策實作灰階釋出。
實作思路如下:
- 在網關的全局過濾器中根據業務規則給流量打上灰階标記
- 将灰階标記放入請求頭中,傳遞給下遊服務
- 改造Ribbon負載均衡政策,根據流量标記從注冊中心擷取灰階服務
- 請求路由轉發
第一個問題:根據什麼條件打上灰階标記?
這個需要根據實際的業務需要,比如根據使用者所在的地區、使用用戶端類型、随機截取流量.....
這裡我将直接使用一個标記grayTag,隻要用戶端請求頭中攜帶了這個參數,并且設定為true,則走灰階釋出邏輯。
請求頭中攜帶:grayTag=true
第二個問題:為什麼要在請求頭中添加灰階标記傳遞給下遊服務?
這一步非常關鍵,實作灰階标記透傳給下遊服務的關鍵,将灰階标記放在請求頭中,下遊服務隻需要從請求頭中擷取灰階标記便知道是否是灰階釋出,這個和令牌中繼一個原理。
第三個問題:灰階标記如何請求隔離?
Spring MVC中的每個請求都是開啟一個線程進行處理,是以可以将灰階标記放置在ThreadLocal中進行線程隔離。
第四個問題:如何知道注冊中心的服務哪個是灰階服務?
Nacos支援在服務中配置一些中繼資料,可以将灰階标記配置在中繼資料中,這樣就能區分哪些是灰階服務,哪些是正常服務。
第五個問題:如何針對特定的服務進行灰階釋出?
比如我的《Spring Cloud Alibaba實戰》中涉及的一條調用鍊路如下圖:
需求:現在隻對文章服務、評論服務進行灰階釋出,其他服務依然使用線上正在運作的服務
此時的調用關系就變成了下圖:
我們知道網關路由中配置的服務很多,如何隻針對文章服務進行灰階釋出呢?
很簡單:隻需要将自定義的Ribbon灰階釋出規則隻對文章服務生效。
這裡涉及到Ribbon中的一個注解:@RibbonClients,隻需要在其中的value屬性指定需要生效的服務名稱,那麼此時網關中的配置如下:
@RibbonClients(value ={
//隻對文章服務進行灰階釋出
@RibbonClient(value = "article-server",configuration = GrayRuleConfig.class)
} )
@SpringBootApplication
public class GatewayApplication{
}
@RibbonClient可以指定多個,這個注解有如下兩個屬性:
- value:指定服務的名稱,在注冊中心配置的服務名稱
- configuration:自定義的負載均衡政策,這裡是灰階釋出的政策
@RibbonClients其中有一個屬性defaultConfiguration,一旦使用這個屬性,那麼灰階釋出的政策對網關路由中配置的所有服務都将生效。
第六個問題:說了這麼多,具體如何實作?
網關中首先需要定義一個全局過濾器,僞代碼如下:
public class GlobalGrayFilter implements GlobalFilter{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain){
//① 解析請求頭,檢視是否存在灰階釋出的請求頭資訊,如果存在則将其放置在ThreadLocal中
HttpHeaders headers = exchange.getRequest().getHeaders();
if (headers.containsKey(GrayConstant.GRAY_HEADER)){
String gray = headers.getFirst(GrayConstant.GRAY_HEADER);
if (StrUtil.equals(gray,GrayConstant.GRAY_VALUE)){
//②設定灰階标記
GrayRequestContextHolder.setGrayTag(true);
}
}
//③ 将灰階标記放入請求頭中
ServerHttpRequest tokenRequest = exchange.getRequest().mutate()
//将灰階标記傳遞過去
.header(GrayConstant.GRAY_HEADER,GrayRequestContextHolder.getGrayTag().toString())
.build();
ServerWebExchange build = exchange.mutate().request(tokenRequest).build();
return
①處的代碼:從請求頭中擷取用戶端傳遞過來的灰階标記(這裡根據自己業務需要自行更改),判斷是否是灰階釋出
②處的代碼:GrayRequestContextHolder則是自定義的ThreadLocal實作的線程隔離工具,用來存放灰階标記
③處的代碼:将灰階标記放置在請求頭中,傳遞給下遊微服務,這裡是和令牌一個邏輯。
注意:這個全局過濾器一定要放在OAuth2.0鑒權過濾器之前,優先級要調高
全局過濾器中已經将灰階标記打上了,放置在GrayRequestContextHolder中,下面隻需要改造Ribbon的負載均衡的政策去注冊中心選擇灰階服務。
建立GrayRule,代碼如下:
/**
* 灰階釋出的規則
*/
public class GrayRule extends ZoneAvoidanceRule{
@Override
public void initWithNiwsConfig(IClientConfig clientConfig){
}
@Override
public Server choose(Object key){
try {
//從ThreadLocal中擷取灰階标記
boolean grayTag = GrayRequestContextHolder.getGrayTag().get();
//擷取所有可用服務
List<Server> serverList = this.getLoadBalancer().getReachableServers();
//灰階釋出的服務
List<Server> grayServerList = new ArrayList<>();
//正常的服務
List<Server> normalServerList = new ArrayList<>();
for(Server server : serverList) {
NacosServer nacosServer = (NacosServer) server;
//從nacos中擷取元素劇進行比對
if(nacosServer.getMetadata().containsKey(GrayConstant.GRAY_HEADER)
&& nacosServer.getMetadata().get(GrayConstant.GRAY_HEADER).equals(GrayConstant.GRAY_VALUE)) {
grayServerList.add(server);
} else {
normalServerList.add(server);
}
}
//如果被标記為灰階釋出,則調用灰階釋出的服務
if(grayTag) {
return originChoose(grayServerList,key);
} else {
return originChoose(normalServerList,key);
}
} finally {
//清除灰階标記
GrayRequestContextHolder.remove();
}
}
private Server originChoose(List<Server> noMetaServerList, Object key){
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(noMetaServerList, key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
}
邏輯很簡單,如下:
- 擷取灰階标記
- 從Nacos注冊中心擷取灰階服務和正常服務
- 根據灰階标記去判斷,如果灰階釋出則選擇特定的灰階服務進行轉發
定義一個配置類,注入改造的灰階政策GrayRule,如下:
/**
* 灰階部署的負載規則配置類
* 注意:這個類一定不要被Spring Boot 掃描進入IOC容器中,一旦掃描進入則對全部的服務都将生效
*/
public class GrayRuleConfig{
@Bean
public GrayRule grayRule(){
return new
注意:這個GrayRuleConfig不能被掃描進入IOC容器,一旦掃描進入則全局生效
因為不僅僅網關需要用到這個灰階釋出政策,凡是涉及到OpenFeign調用的微服務如果需要配置灰階釋出都需要用到,是以這裡陳某定義了一個公用的gray-starter。
經過上述步驟網關的灰階釋出則已經配置完成,此時隻需要通過**@RibbonClients**指定對應哪個服務灰階釋出。
openFeign透傳灰階标記
上面在介紹網關的灰階釋出配置時,是将灰階标記(grayTag=true)放在了請求頭中,是以在下遊服務中需要做的就隻是從請求頭中将灰階标記取出來,然後将其存入GrayRequestContextHolder上下文中。
這樣一來下遊服務中的GrayRule則能從GrayRequestContextHolder擷取到灰階标記,從注冊中心擷取灰階服務進行調用了。
問題來了:如何從請求頭中取出灰階标記?
在介紹OAuth2.0相關知識時,曾經出過一篇文章:實戰!openFeign如何實作全鍊路JWT令牌資訊不丢失?
其中介紹了令牌中繼的解決方案,使用的是openFeign的請求攔截器去配置請求頭資訊。
如上圖:openFeign在調用時并不是用的原先的Request,而是内部建立了一個Request,其中複制了請求的URL、請求參數一些資訊,但是請求頭并沒有複制過去,是以openFeign調用會丢失請求頭中的資訊。
但是可以通過實作RequestInterceptor将原先的請求頭給複制過去,代碼如下:
@Component
@Slf4j
public class FeignRequestInterceptor implements RequestInterceptor{
@Override
public void apply(RequestTemplate template){
HttpServletRequest httpServletRequest = RequestContextUtils.getRequest();
Map<String, String> headers = getHeaders(httpServletRequest);
for (Map.Entry<String, String> entry : headers.entrySet()) {
//② 設定請求頭到新的Request中
template.header(entry.getKey(), entry.getValue());
}
}
/**
* 擷取原請求頭
*/
private Map<String, String> getHeaders(HttpServletRequest request){
Map<String, String> map = new LinkedHashMap<>();
Enumeration<String> enumeration = request.getHeaderNames();
if (enumeration != null) {
while (enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
String value = request.getHeader(key);
//将灰階标記的請求頭透傳給下個服務
if (StrUtil.equals(GrayConstant.GRAY_HEADER,key)&&Boolean.TRUE.toString().equals(value)){
//① 儲存灰階釋出的标記
GrayRequestContextHolder.setGrayTag(true);
map.put(key, value);
}
}
}
return
①處的代碼:從請求頭中擷取灰階釋出的标記,設定到GrayRequestContextHolder上下文中
②處的代碼:将這個請求頭設定到新的Request中,繼續向下遊服務傳遞。
其實配置一下RequestInterceptor就已經完成了,關于灰階釋出政策隻需要複用網關的GrayRule
注意:也需要使用@RibbonClients注解去标注文章服務調用的哪些服務需要灰階釋出。
代碼如下:
@RibbonClients(value = {
//指定對comments這個服務開啟灰階部署
@RibbonClient(value = "comments",configuration = GrayRuleConfig.class)
})
public class ArticleApplication{}
Nacos中服務如何做灰階标記
其實很簡單,分為兩種:
1、在配置檔案中指定,如下:
spring:
cloud:
nacos:
discovery:
metadata:
## 灰階标記
grayTag: true
2、在Nacos中動态的指定灰階标記
總結
- 網關中通過全局過濾器實作灰階打标,将灰階标記放入請求頭中傳遞給下遊服務
- 網關通過自定義的負載均衡政策,從注冊中心擷取灰階服務,進行轉發
- 在openFeign調用時需要從請求頭中擷取灰階标記,放入上下文中
- openFeign調用同樣是根據自定義的負載均衡政策從注冊中心擷取灰階服務,進行調用。