<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ā)布方案

          共 10404字,需瀏覽 21分鐘

           ·

          2021-08-22 01:50

          關(guān)注我們,設(shè)為星標,每天7:30不見不散,架構(gòu)路上與您共享 

          回復(fù)"架構(gòu)師"獲取資源

          前言

               在平時的業(yè)務(wù)開發(fā)過程中,后端服務(wù)與服務(wù)之間的調(diào)用往往通過fegin或者RestTemplate兩種方式。但是我們在調(diào)用服務(wù)的時候往往只需要寫服務(wù)名就可以做到路由到具體的服務(wù),這其中的原理相比大家都知道是SpringCloudribbon組件幫我們做了負載均衡的功能。

          灰度發(fā)布的核心就是路由,如果我們能夠重寫ribbon默認的負載均衡算法是不是就意味著我們能夠控制服務(wù)的轉(zhuǎn)發(fā)呢?

          是的!

          調(diào)用鏈分析

          外部調(diào)用

          • 請求==>zuul==>服務(wù)

          zuul在轉(zhuǎn)發(fā)請求的時候,也會根據(jù) Ribbon從服務(wù)實例列表中選擇一個對應(yīng)的服務(wù),然后選擇轉(zhuǎn)發(fā).

          內(nèi)部調(diào)用

          • 請求==>zuul==>服務(wù)Resttemplate調(diào)用==>服務(wù)

          • 請求==>zuul==>服務(wù)Fegin調(diào)用==>服務(wù)

          無論是通過 Resttemplate還是 Fegin的方式進行服務(wù)間的調(diào)用,他們都會從 Ribbon選擇一個服務(wù)實例返回.

              上面幾種調(diào)用方式應(yīng)該涵蓋了我們平時調(diào)用中的場景,無論是通過哪種方式調(diào)用(排除直接ip:port調(diào)用),最后都會通過Ribbon,然后返回服務(wù)實例.

          預(yù)備知識

          eureka元數(shù)據(jù)

          Eureka的元數(shù)據(jù)有兩種,分別為標準元數(shù)據(jù)和自定義元數(shù)據(jù)。

          標準元數(shù)據(jù):主機名、IP地址、端口號、狀態(tài)頁和健康檢查等信息,這些信息都會被發(fā)布在服務(wù)注冊表中,用于服務(wù)之間的調(diào)用。

          自定義元數(shù)據(jù):自定義元數(shù)據(jù)可以使用eureka.instance.metadata-map配置,這些元數(shù)據(jù)可以在遠程客戶端中訪問,但是一般不會改變客戶端的行為,除非客戶端知道該元數(shù)據(jù)的含義

          eureka RestFul接口

          請求名稱請求方式HTTP地址請求描述
          注冊新服務(wù)POST/eureka/apps/{appID}傳遞JSON或者XML格式參數(shù)內(nèi)容,HTTP code為204時表示成功
          取消注冊服務(wù)DELETE/eureka/apps/{appID}/{instanceID}HTTP code為200時表示成功
          發(fā)送服務(wù)心跳PUT/eureka/apps/{appID}/{instanceID}HTTP code為200時表示成功
          查詢所有服務(wù)GET/eureka/appsHTTP code為200時表示成功,返回XML/JSON數(shù)據(jù)內(nèi)容
          查詢指定appID的服務(wù)列表GET/eureka/apps/{appID}HTTP code為200時表示成功,返回XML/JSON數(shù)據(jù)內(nèi)容
          查詢指定appID&instanceIDGET/eureka/apps/{appID}/{instanceID}獲取指定appID以及InstanceId的服務(wù)信息,HTTP code為200時表示成功,返回XML/JSON數(shù)據(jù)內(nèi)容
          查詢指定instanceID服務(wù)列表GET/eureka/apps/instances/{instanceID}獲取指定instanceID的服務(wù)列表,HTTP code為200時表示成功,返回XML/JSON數(shù)據(jù)內(nèi)容
          變更服務(wù)狀態(tài)PUT/eureka/apps/{appID}/{instanceID}/status?value=DOWN服務(wù)上線、服務(wù)下線等狀態(tài)變動,HTTP code為200時表示成功
          變更元數(shù)據(jù)PUT/eureka/apps/{appID}/{instanceID}/metadata?key=valueHTTP code為200時表示成功

          更改自定義元數(shù)據(jù)

          配置文件方式:

          eureka.instance.metadata-map.version = v1

          接口請求:

          PUT /eureka/apps/{appID}/{instanceID}/metadata?key=value

          實現(xiàn)流程

          1. 用戶請求首先到達Nginx然后轉(zhuǎn)發(fā)到網(wǎng)關(guān)zuul,此時zuul攔截器會根據(jù)用戶攜帶請求token解析出對應(yīng)的userId

          2. 網(wǎng)關(guān)從Apollo配置中心拉取灰度用戶列表,然后根據(jù)灰度用戶策略判斷該用戶是否是灰度用戶。如是,則給該請求添加請求頭線程變量添加信息version=xxx;若不是,則不做任何處理放行

          3. zuul攔截器執(zhí)行完畢后,zuul在進行轉(zhuǎn)發(fā)請求時會通過負載均衡器Ribbon。

          4. 負載均衡Ribbon被重寫。當請求到達時候,Ribbon會取出zuul存入線程變量version。于此同時,Ribbon還會取出所有緩存的服務(wù)列表(定期從eureka刷新獲取最新列表)及其該服務(wù)的metadata-map信息。然后取出服務(wù)metadata-mapversion信息與線程變量version進行判斷對比,若值一直則選擇該服務(wù)作為返回。若所有服務(wù)列表的version信息與之不匹配,則返回null,此時Ribbon選取不到對應(yīng)的服務(wù)則會報錯!

          5. 當服務(wù)為非灰度服務(wù),即沒有version信息時,此時Ribbon會收集所有非灰度服務(wù)列表,然后利用Ribbon默認的規(guī)則從這些非灰度服務(wù)列表中返回一個服務(wù)。


          6. zuul通過Ribbon將請求轉(zhuǎn)發(fā)到consumer服務(wù)后,可能還會通過feginresttemplate調(diào)用其他服務(wù),如provider服務(wù)。但是無論是通過fegin還是resttemplate,他們最后在選取服務(wù)轉(zhuǎn)發(fā)的時候都會通過Ribbon

          7. 那么在通過feginresttemplate調(diào)用另外一個服務(wù)的時候需要設(shè)置一個攔截器,將請求頭version=xxx給帶上,然后存入線程變量。

          8. 在經(jīng)過feginresttemplate 的攔截器后最后會到Ribbon,Ribbon會從線程變量里面取出version信息。然后重復(fù)步驟(4)和(5)

          設(shè)計思路

          首先,我們通過更改服務(wù)在eureka的元數(shù)據(jù)標識該服務(wù)為灰度服務(wù),筆者這邊用的元數(shù)據(jù)字段為version 

          1.首先更改服務(wù)元數(shù)據(jù)信息,標記其灰度版本。通過eureka RestFul接口或者配置文件添加如下信息eureka.instance.metadata-map.version=v1

          2.自定義zuul攔截器GrayFilter。此處筆者獲取的請求頭為token,然后將根據(jù)JWT的思想獲取userId,然后獲取灰度用戶列表及其灰度版本信息,判斷該用戶是否為灰度用戶。

          若為灰度用戶,則將灰度版本信息version存放在線程變量里面。此處不能用Threadlocal存儲線程變量,因為SpringCloud用hystrix做線程池隔離,而線程池是無法獲取到ThreadLocal中的信息的! 所以這個時候我們可以參考Sleuth做分布式鏈路追蹤的思路或者使用阿里開源的TransmittableThreadLocal方案。此處使用HystrixRequestVariableDefault實現(xiàn)跨線程池傳遞線程變量。

          3.zuul攔截器處理完畢后,會經(jīng)過ribbon組件從服務(wù)實例列表中獲取一個實例選擇轉(zhuǎn)發(fā)。Ribbon默認的Rule為ZoneAvoidanceRule`。而此處我們繼承該類,重寫了其父類選擇服務(wù)實例的方法。

          以下為Ribbon源碼:

          public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
             // 略....
              @Override
              public Server choose(Object key) {
                  ILoadBalancer lb = getLoadBalancer();
                  Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
                  if (server.isPresent()) {
                      return server.get();
                  } else {
                      return null;
                  }       
              }
          }

          以下為自定義實現(xiàn)的偽代碼:

          public class GrayMetadataRule extends ZoneAvoidanceRule {
             // 略....
              @Override
              public Server choose(Object key) {
                //1.從線程變量獲取version信息
                  String version = HystrixRequestVariableDefault.get();
                  
                //2.獲取服務(wù)實例列表
                  List<Server> serverList = this.getPredicate().getEligibleServers(this.getLoadBalancer().getAllServers(), key);
                  
                 //3.循環(huán)serverList,選擇version匹配的服務(wù)并返回
                          for (Server server : serverList) {
                      Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
           
                      String metaVersion = metadata.get("version);
                      if (!StringUtils.isEmpty(metaVersion)) {
                          if (metaVersion.equals(hystrixVer)) {
                              return server;
                          }
                      }
                  }
              }
          }

          4.此時,只是已經(jīng)完成了 請求==》zuul==》zuul攔截器==》自定義ribbon負載均衡算法==》灰度服務(wù)這個流程,并沒有涉及到 服務(wù)==》服務(wù)的調(diào)用。

          服務(wù)到服務(wù)的調(diào)用無論是通過resttemplate還是fegin,最后也會走ribbon的負載均衡算法,即服務(wù)==》Ribbon 自定義Rule==》服務(wù)。因為此時自定義的GrayMetadataRule并不能從線程變量中取到version,因為已經(jīng)到了另外一個服務(wù)里面了。

          5.此時依然可以參考Sleuth的源碼org.springframework.cloud.sleuth.Span,這里不做贅述只是大致講一下該類的實現(xiàn)思想。就是在請求里面添加請求頭,以便下個服務(wù)能夠從請求頭中獲取信息。

          此處,我們可以通過在 步驟2中,讓zuul添加添加線程變量的時候也在請求頭中添加信息。然后,再自定義HandlerInterceptorAdapter攔截器,使之在到達服務(wù)之前將請求頭中的信息存入到線程變量HystrixRequestVariableDefault中。

          然后服務(wù)再調(diào)用另外一個服務(wù)之前,設(shè)置resttemplate和fegin的攔截器,添加頭信息。

          resttemplate攔截器

          public class CoreHttpRequestInterceptor implements ClientHttpRequestInterceptor {
              @Override
              public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
                  HttpRequestWrapper requestWrapper = new HttpRequestWrapper(request);
                  String hystrixVer = CoreHeaderInterceptor.version.get();
                  requestWrapper.getHeaders().add(CoreHeaderInterceptor.HEADER_VERSION, hystrixVer);
                  return execution.execute(requestWrapper, body);
              }
          }

          fegin攔截器

          public class CoreFeignRequestInterceptor implements RequestInterceptor {
             @Override
             public void apply(RequestTemplate template) {
                  String hystrixVer = CoreHeaderInterceptor.version.get();
                  logger.debug("====>fegin version:{} ",hystrixVer); 
                template.header(CoreHeaderInterceptor.HEADER_VERSION, hystrixVer);
             }
           
          }

          6.到這里基本上整個請求流程就比較完整了,但是我們怎么讓Ribbon使用自定義的Rule?這里其實非常簡單,只需要在服務(wù)的配置文件中配置一下代碼即可.

          yourServiceId.ribbon.NFLoadBalancerRuleClassName=自定義的負載均衡策略類

          但是這樣配置需要指定服務(wù)名,意味著需要在每個服務(wù)的配置文件中這么配置一次,所以需要對此做一下擴展.打開源碼org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration類,該類是Ribbon的默認配置類.可以清楚的發(fā)現(xiàn)該類注入了一個PropertiesFactory類型的屬性,可以看到PropertiesFactory類的構(gòu)造方法

              public PropertiesFactory() {
                  classToProperty.put(ILoadBalancer.class, "NFLoadBalancerClassName");
                  classToProperty.put(IPing.class, "NFLoadBalancerPingClassName");
                  classToProperty.put(IRule.class, "NFLoadBalancerRuleClassName");
                  classToProperty.put(ServerList.class, "NIWSServerListClassName");
                  classToProperty.put(ServerListFilter.class, "NIWSServerListFilterClassName");
              }

          所以,我們可以繼承該類從而實現(xiàn)我們的擴展,這樣一來就不用配置具體的服務(wù)名了.至于Ribbon是如何工作的,這里有一篇方志明的文章(傳送門)可以加強對Ribbon工作機制的理解

          7.到這里基本上整個請求流程就比較完整了,上述例子中是以用戶ID作為灰度的維度,當然這里可以實現(xiàn)更多的灰度策略,比如IP等,基本上都可以基于此方式做擴展

          灰度使用

          配置文件示例

          spring.application.name = provide-test
          server.port = 7770
          eureka.client.service-url.defaultZone = http://localhost:1111/eureka/
           
          #啟動后直接將該元數(shù)據(jù)信息注冊到eureka
          #eureka.instance.metadata-map.version = v1

          測試案例

          分別啟動四個測試實例,有version代表灰度服務(wù),無version則為普通服務(wù)。當灰度服務(wù)測試沒問題的時候,通過PUT請求eureka接口將version信息去除,使其變成普通服務(wù).

          實例列表

          • [x] zuul-server

          • [x] provider-test
            port:7770 version:無
            port: 7771 version:v1

          • [x] consumer-test

            port:8880 version:無

            port: 8881 version:v1

          修改服務(wù)信息

          服務(wù)在eureka的元數(shù)據(jù)信息可通過接口http://localhost:1111/eureka/apps訪問到。

          服務(wù)信息實例:

          訪問接口查看信息http://localhost:1111/eureka/apps/PROVIDE-TEST

          注意事項

          通過此種方法更改server的元數(shù)據(jù)后,由于ribbon會緩存實力列表,所以在測試改變服務(wù)信息時,ribbon并不會立馬從eureka拉去最新信息m,這個拉取信息的時間可自行配置。

          同時,當服務(wù)重啟時服務(wù)會重新將配置文件的version信息注冊上去。

          測試演示

          zuul==>provider服務(wù)

          用戶andy為灰度用戶。 
          1.測試灰度用戶andy,是否路由到灰度服務(wù) 
          provider-test:7771 
          2.測試非灰度用戶andyaaa(任意用戶)是否能被路由到普通服務(wù) 
          provider-test:7770

          zuul==>consumer服務(wù)>provider服務(wù)

          以同樣的方式再啟動兩個consumer-test服務(wù),這里不再截圖演示。

          請求從zuul==>consumer-test==>provider-test,通過feginresttemplate兩種請求方式測試

          Resttemplate請求方式

          fegin請求方式

          自動化配置

          與Apollo實現(xiàn)整合,避免手動調(diào)用接口。實現(xiàn)配置監(jiān)聽,完成灰度。


          本文鏈接:

          https://blog.csdn.net/dupengcheng1/article/details/89187452


          瀏覽 62
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  国产黄色片免费在线观看 | 欧美精品手机在线 | 日韩一级aV毛片 | 丁香五月综合激清 | 欧美黑人一级A片免费看 |