<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          聊聊 Spring Cloud 全鏈路灰度發(fā)布 方案~

          共 7645字,需瀏覽 16分鐘

           ·

          2022-05-12 22:33


          源?/?? ? ? ??文/?


          實際生產(chǎn)中如有需求變更,并不會直接更新線上服務(wù),最通常的做法便是:切出線上的小部分流量進行體驗測試,經(jīng)過測試后無問題則全面的上線。

          這樣做的好處也是非常明顯,一旦出現(xiàn)了BUG,能夠保證大部分的客戶端正常使用。

          要實現(xiàn)這種平滑過渡的方式就需要用到本篇文章介紹到的全鏈路灰度發(fā)布

          什么是灰度發(fā)布?

          灰度發(fā)布(又名金絲雀發(fā)布)是指在黑與白之間,能夠平滑過渡的一種發(fā)布方式。在其上可以進行A/B testing,即讓一部分用戶繼續(xù)用產(chǎn)品特性A,一部分用戶開始用產(chǎn)品特性B,如果用戶對B沒有什么反對意見,那么逐步擴大范圍,把所有用戶都遷移到B上面來。灰度發(fā)布可以保證整體系統(tǒng)的穩(wěn)定,在初始灰度的時候就可以發(fā)現(xiàn)、調(diào)整問題,以保證其影響度。

          為什么是全鏈路灰度發(fā)布?

          在陳某前面一篇文章有介紹到網(wǎng)關(guān)的灰度發(fā)布實現(xiàn),僅僅是實現(xiàn)了網(wǎng)關(guān)路由轉(zhuǎn)發(fā)的灰度發(fā)布,如下圖:

          如上圖,網(wǎng)關(guān)灰度發(fā)布實現(xiàn)的是網(wǎng)關(guān)通過灰度標記路由到文章服務(wù)B(灰度服務(wù)),至于從文章服務(wù)B到評論服務(wù)是通過openFeign內(nèi)部調(diào)用的,默認無法實現(xiàn)灰度標記grayTag的透傳,因此文章服務(wù)B最終調(diào)用的是評論服務(wù)A,并不是評論服務(wù)B。

          全鏈路灰度發(fā)布需要實現(xiàn)的是:

          1. 網(wǎng)關(guān)通過灰度標記將部分流量轉(zhuǎn)發(fā)給文章服務(wù)B
          2. 文章服務(wù)B能夠?qū)崿F(xiàn)灰度標記grayTag的透傳,最終調(diào)用評論服務(wù)B

          經(jīng)過以上分析,全鏈路灰度發(fā)布需要實現(xiàn)兩個點:

          1. 網(wǎng)關(guān)路由轉(zhuǎn)發(fā)實現(xiàn)灰度發(fā)布
          2. 服務(wù)內(nèi)部通過openFeign調(diào)用實現(xiàn)灰度發(fā)布(透傳灰度標記grayTag)。

          下面將以陳某的《Spring Cloud Alibaba實戰(zhàn)》專欄中的服務(wù)為例進行灰度發(fā)布配置。

          網(wǎng)關(guān)層的灰度路由轉(zhuǎn)發(fā)

          本篇文章將使用Ribbon+Spring Cloud Gateway?進行改造負載均衡策略實現(xiàn)灰度發(fā)布。

          實現(xiàn)思路如下:

          1. 在網(wǎng)關(guān)的全局過濾器中根據(jù)業(yè)務(wù)規(guī)則給流量打上灰度標記
          2. 將灰度標記放入請求頭中,傳遞給下游服務(wù)
          3. 改造Ribbon負載均衡策略,根據(jù)流量標記從注冊中心獲取灰度服務(wù)
          4. 請求路由轉(zhuǎn)發(fā)

          第一個問題:根據(jù)什么條件打上灰度標記?

          這個需要根據(jù)實際的業(yè)務(wù)需要,比如根據(jù)用戶所在的地區(qū)、使用客戶端類型、隨機截取流量.....

          這里我將直接使用一個標記grayTag,只要客戶端請求頭中攜帶了這個參數(shù),并且設(shè)置為true,則走灰度發(fā)布邏輯。

          請求頭中攜帶:grayTag=true

          第二個問題:為什么要在請求頭中添加灰度標記傳遞給下游服務(wù)?

          這一步非常關(guān)鍵,實現(xiàn)灰度標記透傳給下游服務(wù)的關(guān)鍵,將灰度標記放在請求頭中,下游服務(wù)只需要從請求頭中獲取灰度標記便知道是否是灰度發(fā)布,這個和令牌中繼一個原理。

          第三個問題:灰度標記如何請求隔離?

          Spring MVC中的每個請求都是開啟一個線程進行處理,因此可以將灰度標記放置在ThreadLocal中進行線程隔離。

          第四個問題:如何知道注冊中心的服務(wù)哪個是灰度服務(wù)?

          Nacos支持在服務(wù)中配置一些元數(shù)據(jù),可以將灰度標記配置在元數(shù)據(jù)中,這樣就能區(qū)分哪些是灰度服務(wù),哪些是正常服務(wù)。

          第五個問題:如何針對特定的服務(wù)進行灰度發(fā)布?

          比如我的《Spring Cloud Alibaba實戰(zhàn)》中涉及的一條調(diào)用鏈路如下圖:

          需求:現(xiàn)在只對文章服務(wù)評論服務(wù)進行灰度發(fā)布,其他服務(wù)依然使用線上正在運行的服務(wù)

          此時的調(diào)用關(guān)系就變成了下圖:

          我們知道網(wǎng)關(guān)路由中配置的服務(wù)很多,如何只針對文章服務(wù)進行灰度發(fā)布呢?

          很簡單:只需要將自定義的Ribbon灰度發(fā)布規(guī)則只對文章服務(wù)生效。

          這里涉及到Ribbon中的一個注解:@RibbonClients?,只需要在其中的value屬性指定需要生效的服務(wù)名稱,那么此時網(wǎng)關(guān)中的配置如下:

          @RibbonClients(value?={
          ????????//只對文章服務(wù)進行灰度發(fā)布
          ????????@RibbonClient(value?=?"article-server",configuration?=?GrayRuleConfig.class)
          }?)
          @SpringBootApplication
          public?class?GatewayApplication?
          {
          ???
          }

          @RibbonClient可以指定多個,這個注解有如下兩個屬性:

          • value:指定服務(wù)的名稱,在注冊中心配置的服務(wù)名稱
          • configuration:自定義的負載均衡策略,這里是灰度發(fā)布的策略

          @RibbonClients其中有一個屬性defaultConfiguration,一旦使用這個屬性,那么灰度發(fā)布的策略對網(wǎng)關(guān)路由中配置的所有服務(wù)都將生效。

          第六個問題:說了這么多,具體如何實現(xiàn)?

          網(wǎng)關(guān)中首先需要定義一個全局過濾器,偽代碼如下:

          public?class?GlobalGrayFilter?implements?GlobalFilter{
          ????@Override
          ????public?Mono?filter(ServerWebExchange?exchange,?GatewayFilterChain?chain)?{
          ?????????//①?解析請求頭,查看是否存在灰度發(fā)布的請求頭信息,如果存在則將其放置在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)){
          ????????????????//②設(shè)置灰度標記
          ????????????????GrayRequestContextHolder.setGrayTag(true);
          ????????????}
          ????????}
          ???????//③?將灰度標記放入請求頭中
          ???ServerHttpRequest?tokenRequest?=?exchange.getRequest().mutate()
          ????//將灰度標記傳遞過去
          ????.header(GrayConstant.GRAY_HEADER,GrayRequestContextHolder.getGrayTag().toString())
          ????.build();
          ????????????ServerWebExchange?build?=?exchange.mutate().request(tokenRequest).build();
          ????????????return?chain.filter(build);
          ????}
          }

          ①處的代碼:從請求頭中獲取客戶端傳遞過來的灰度標記(這里根據(jù)自己業(yè)務(wù)需要自行更改),判斷是否是灰度發(fā)布

          ②處的代碼GrayRequestContextHolder則是自定義的ThreadLocal實現(xiàn)的線程隔離工具,用來存放灰度標記

          ③處的代碼:將灰度標記放置在請求頭中,傳遞給下游微服務(wù),這里是和令牌一個邏輯。

          注意:這個全局過濾器一定要放在OAuth2.0鑒權(quán)過濾器之前,優(yōu)先級要調(diào)高

          全局過濾器中已經(jīng)將灰度標記打上了,放置在GrayRequestContextHolder中,下面只需要改造Ribbon的負載均衡的策略去注冊中心選擇灰度服務(wù)。

          創(chuàng)建GrayRule,代碼如下:

          /**
          ?*?灰度發(fā)布的規(guī)則
          ?*/

          public?class?GrayRule?extends?ZoneAvoidanceRule?{

          ????@Override
          ????public?void?initWithNiwsConfig(IClientConfig?clientConfig)?{
          ????}

          ????@Override
          ????public?Server?choose(Object?key)?{
          ????????try?{
          ????????????//從ThreadLocal中獲取灰度標記
          ????????????boolean?grayTag?=?GrayRequestContextHolder.getGrayTag().get();
          ????????????//獲取所有可用服務(wù)
          ????????????List?serverList?=?this.getLoadBalancer().getReachableServers();
          ????????????//灰度發(fā)布的服務(wù)
          ????????????List?grayServerList?=?new?ArrayList<>();
          ????????????//正常的服務(wù)
          ????????????List?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);
          ????????????????}
          ????????????}
          ????????????//如果被標記為灰度發(fā)布,則調(diào)用灰度發(fā)布的服務(wù)
          ????????????if(grayTag)?{
          ????????????????return?originChoose(grayServerList,key);
          ????????????}?else?{
          ????????????????return?originChoose(normalServerList,key);
          ????????????}
          ????????}?finally?{
          ????????????//清除灰度標記
          ????????????GrayRequestContextHolder.remove();
          ????????}
          ????}

          ????private?Server?originChoose(List?noMetaServerList,?Object?key)?{
          ????????Optional?server?=?getPredicate().chooseRoundRobinAfterFiltering(noMetaServerList,?key);
          ????????if?(server.isPresent())?{
          ????????????return?server.get();
          ????????}?else?{
          ????????????return?null;
          ????????}
          ????}
          }

          邏輯很簡單,如下:

          1. 獲取灰度標記
          2. 從Nacos注冊中心獲取灰度服務(wù)和正常服務(wù)
          3. 根據(jù)灰度標記去判斷,如果灰度發(fā)布則選擇特定的灰度服務(wù)進行轉(zhuǎn)發(fā)

          定義一個配置類,注入改造的灰度策略GrayRule,如下:

          /**
          ?*?灰度部署的負載規(guī)則配置類
          ?*?注意:這個類一定不要被Spring Boot 掃描進入IOC容器中,一旦掃描進入則對全部的服務(wù)都將生效
          ?*/

          public?class?GrayRuleConfig?{
          ????@Bean
          ????public?GrayRule?grayRule(){
          ????????return?new?GrayRule();
          ????}
          }

          注意:這個GrayRuleConfig不能被掃描進入IOC容器,一旦掃描進入則全局生效

          因為不僅僅網(wǎng)關(guān)需要用到這個灰度發(fā)布策略,凡是涉及到OpenFeign調(diào)用的微服務(wù)如果需要配置灰度發(fā)布都需要用到,因此這里陳某定義了一個公用的gray-starter

          經(jīng)過上述步驟網(wǎng)關(guān)的灰度發(fā)布則已經(jīng)配置完成,此時只需要通過@RibbonClients指定對應(yīng)哪個服務(wù)灰度發(fā)布。

          openFeign透傳灰度標記

          上面在介紹網(wǎng)關(guān)的灰度發(fā)布配置時,是將灰度標記(grayTag=true)放在了請求頭中,因此在下游服務(wù)中需要做的就只是從請求頭中將灰度標記取出來,然后將其存入GrayRequestContextHolder上下文中。

          這樣一來下游服務(wù)中的GrayRule則能從GrayRequestContextHolder獲取到灰度標記,從注冊中心獲取灰度服務(wù)進行調(diào)用了。

          問題來了:如何從請求頭中取出灰度標記?

          在介紹OAuth2.0相關(guān)知識時,曾經(jīng)出過一篇文章:實戰(zhàn)!openFeign如何實現(xiàn)全鏈路JWT令牌信息不丟失?

          其中介紹了令牌中繼的解決方案,使用的是openFeign的請求攔截器去配置請求頭信息。

          如上圖:openFeign在調(diào)用時并不是用的原先的Request,而是內(nèi)部新建了一個Request,其中復(fù)制了請求的URL、請求參數(shù)一些信息,但是請求頭并沒有復(fù)制過去,因此openFeign調(diào)用會丟失請求頭中的信息。

          但是可以通過實現(xiàn)RequestInterceptor將原先的請求頭給復(fù)制過去,代碼如下:

          @Component
          @Slf4j
          public?class?FeignRequestInterceptor?implements?RequestInterceptor?{
          ????@Override
          ????public?void?apply(RequestTemplate?template)?{
          ????????HttpServletRequest?httpServletRequest?=?RequestContextUtils.getRequest();
          ????????Map?headers?=?getHeaders(httpServletRequest);
          ????????for?(Map.Entry?entry?:?headers.entrySet())?{
          ????????????//②?設(shè)置請求頭到新的Request中
          ????????????template.header(entry.getKey(),?entry.getValue());
          ????????}
          ????}

          ????/**
          ?????*?獲取原請求頭
          ?????*/

          ????private?Map?getHeaders(HttpServletRequest?request)?{
          ????????Map?map?=?new?LinkedHashMap<>();
          ????????Enumeration?enumeration?=?request.getHeaderNames();
          ????????if?(enumeration?!=?null)?{
          ????????????while?(enumeration.hasMoreElements())?{
          ????????????????String?key?=?enumeration.nextElement();
          ????????????????String?value?=?request.getHeader(key);
          ????????????????//將灰度標記的請求頭透傳給下個服務(wù)
          ????????????????if?(StrUtil.equals(GrayConstant.GRAY_HEADER,key)&&Boolean.TRUE.toString().equals(value)){
          ????????????????????//①?保存灰度發(fā)布的標記
          ????????????????????GrayRequestContextHolder.setGrayTag(true);
          ????????????????????map.put(key,?value);
          ????????????????}
          ????????????}
          ????????}
          ????????return?map;
          ????}
          }

          ①處的代碼:從請求頭中獲取灰度發(fā)布的標記,設(shè)置到GrayRequestContextHolder上下文中

          ②處的代碼:將這個請求頭設(shè)置到新的Request中,繼續(xù)向下游服務(wù)傳遞。

          其實配置一下RequestInterceptor就已經(jīng)完成了,關(guān)于灰度發(fā)布策略只需要復(fù)用網(wǎng)關(guān)的GrayRule

          注意:也需要使用@RibbonClients注解去標注文章服務(wù)調(diào)用的哪些服務(wù)需要灰度發(fā)布。

          代碼如下:

          @RibbonClients(value?=?{
          ????????//指定對comments這個服務(wù)開啟灰度部署
          ????????@RibbonClient(value?=?"comments",configuration?=?GrayRuleConfig.class)
          })
          public?class?ArticleApplication?
          {}

          Nacos中服務(wù)如何做灰度標記

          其實很簡單,分為兩種:

          1、在配置文件中指定,如下:

          spring:
          ??cloud:
          ????nacos:
          ??????discovery:
          ????????metadata:
          ??????????##?灰度標記
          ??????????grayTag:?true

          2、在Nacos中動態(tài)的指定灰度標記

          配置完成之后,在客戶端請求的時候只需要攜帶grayTag=true這個請求頭即可調(diào)用灰度服務(wù)。

          總結(jié)

          微服務(wù)中全鏈路灰度發(fā)布方案其實很簡單,重要的就是灰度打標,整體流程如下:

          1. 網(wǎng)關(guān)中通過全局過濾器實現(xiàn)灰度打標,將灰度標記放入請求頭中傳遞給下游服務(wù)
          2. 網(wǎng)關(guān)通過自定義的負載均衡策略,從注冊中心獲取灰度服務(wù),進行轉(zhuǎn)發(fā)
          3. 在openFeign調(diào)用時需要從請求頭中獲取灰度標記,放入上下文中
          4. openFeign調(diào)用同樣是根據(jù)自定義的負載均衡策略從注冊中心獲取灰度服務(wù),進行調(diào)用。



          end





          頂級程序員:topcoding

          做最好的程序員社區(qū):Java后端開發(fā)、Python、大數(shù)據(jù)、AI


          一鍵三連「分享」、「點贊」和「在看」



          瀏覽 20
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美激情视频一区二区三区不卡 | 影音先锋中文资源网 | 在线播放,日韩专区 | 毛片色毛片18毛片 | 久久久久国产精品夜夜夜夜夜 |