2021升級版微服務教程6—Ribbon使用+原理+整合Nacos權重+實戰優化 一篇搞定

程式設計鹿發表於2021-01-14

2021升級版SpringCloud教程從入門到實戰精通「H版&alibaba&鏈路追蹤&日誌&事務&鎖」

教程全目錄「含視訊」:https://gitee.com/bingqilinpeishenme/Java-Wiki

Ribbon使用+原理+整合Nacos權重+實戰優化 一篇搞定

Ribbon基本使用

簡介

Ribbon是一個客戶端負載均衡工具,封裝Netflix Ribbon元件,能夠提供客戶端負載均衡能力。

理解Ribbon最重要的就是理解客戶端這個概念,所謂客戶端負載均衡工具不同於Nginx(服務端負載均衡),Ribbon和應用程式繫結,本身不是獨立的服務,也不儲存服務列表,需要負載均衡的時候,會通過應用程式獲取註冊服務列表,然後通過列表進行負載均衡和呼叫。

  • Nginx獨立程式做負載均衡,通過負載均衡策略,將請求轉發到不同的服務上
  • 客戶端負載均衡,通過在客戶端儲存服務列表資訊,然後自己呼叫負載均衡策略,分攤呼叫不同的服務

基本使用

Ribbon的負載均衡有兩種方式

  1. 和 RestTemplate 結合 Ribbon+RestTemplate
  2. 和 OpenFeign 結合

Ribbon的核心子模組

  1. ribbon-loadbalancer:可以獨立使用或者和其他模組一起使用的負載均衡API
  2. ribbon-core:Ribbon的核心API

訂單服務整合Ribbon

訂單服務呼叫商品服務

配置過程 分兩步

  1. 在訂單服務中匯入ribbon的依賴

    <!--ribbon-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
    </dependency>
  2. 配置 RestTemplate

    1594002129980
    1594002129980

訂單服務呼叫商品服務

  1. 訂單服務呼叫商品服務的連結 不能寫成ip+埠號,需要寫成商品服務的服務名稱

    1594002347393
    1594002347393
  2. 重啟 訂單服務 測試負載均衡

    1594002358421
    1594002358421
    1594002385509
    1594002385509

Ribbon負載均衡簡單版實現的流程

  1. RestTemplate傳送的請求是服務名稱http://nacos-product/product/getProductById/1
  2. 獲取@LoadBalanced註解標記的RestTemplate
  3. RestTemplate新增一個攔截器,當使用RestTemplate發起http呼叫時進行攔截
  4. 根據url中的服務名稱 以及自身的負載均衡策略 去訂單服務的服務列表中找到一個要呼叫的ip+埠號 localhost:8802
  5. 訪問該目標服務,並獲取返回結果
1594002530793
1594002530793

服務列表實際上是個map

image-20210106112037719
image-20210106112037719

Ribbon負載均衡原理 [瞭解]

獲取@LoadBalanced註解標記的RestTemplate。

Ribbon將所有標記@LoadBalanced註解的RestTemplate儲存到一個List集合當中,具體原始碼如下:

@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();

具體原始碼位置是在LoadBalancerAutoConfiguration中。

RestTemplate新增一個攔截器

攔截器不是Ribbon的功能

RestTemplate新增攔截器需要有兩個步驟,首先是定義一個攔截器,其次是將定義的攔截器新增到RestTemplate中。

定義一個攔截器

實現ClientHttpRequestInterceptor介面就具備了攔截請求的功能,該介面原始碼如下:

public interface ClientHttpRequestInterceptor {
    /**
     *實現該方法,在該方法內完成攔截請求後的邏輯內容。
     *對於ribbon而言,在該方法內完成了根據具體規則從
     *服務叢集中選取一個服務,並向該服務發起請求的操作。
     */

   ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException;

}

ribbon中對應的實現類是LoadBalancerInterceptor具體原始碼如下:

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

   private LoadBalancerClient loadBalancer;
   private LoadBalancerRequestFactory requestFactory;

    //省略構造器程式碼...

   @Override
   public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
         final ClientHttpRequestExecution execution)
 throws IOException 
{
      final URI originalUri = request.getURI();
      String serviceName = originalUri.getHost();
      /**
       *攔截請求,並呼叫loadBalancer.execute()方法
       *在該方法內部完成server的選取。向選取的server
       *發起請求,並獲得返回結果。
       */

      return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
   }
}

將攔截器新增到RestTemplate中

RestTemplate繼承了InterceptingHttpAccessor,在InterceptingHttpAccessor中提供了獲取以及新增攔截器的方法,具體原始碼如下:

public abstract class InterceptingHttpAccessor extends HttpAccessor {

    /**
     * 所有的攔截器是以一個List集合形式進行儲存。
     */

   private List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();

   /**
    * 設定攔截器。
    */

   public void setInterceptors(List<ClientHttpRequestInterceptor> interceptors) {
      this.interceptors = interceptors;
   }

   /**
    * 獲取當前的攔截器。
    */

   public List<ClientHttpRequestInterceptor> getInterceptors() {
      return interceptors;
   }

   //省略部分程式碼...
}

通過這兩個方法我們就可以將剛才定義的LoadBalancerInterceptor新增到有@LoadBalanced註解標識的RestTemplate中。具體的原始碼如下(LoadBalancerAutoConfiguration)省略部分程式碼:

public class LoadBalancerAutoConfiguration {

    /**
      * 獲取所有帶有@LoadBalanced註解的restTemplate
     */

   @LoadBalanced
   @Autowired(required = false)
   private List<RestTemplate> restTemplates = Collections.emptyList();

    /**
     * 建立SmartInitializingSingleton介面的實現類。Spring會在所有
     * 單例Bean初始化完成後回撥該實現類的afterSingletonsInstantiated()
     * 方法。在這個方法中會為所有被@LoadBalanced註解標識的
     * RestTemplate新增ribbon的自定義攔截器LoadBalancerInterceptor。
     */

   @Bean
   public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
         final List<RestTemplateCustomizer> customizers)
 
{
      return new SmartInitializingSingleton() {
         @Override
         public void afterSingletonsInstantiated() {
            for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
               for (RestTemplateCustomizer customizer : customizers) {
                  customizer.customize(restTemplate);
               }
            }
         }
      };
   }
    /**
     * 建立Ribbon自定義攔截器LoadBalancerInterceptor
     * 建立前提是當前classpath下不存在spring-retry。
     * 所以LoadBalancerInterceptor是預設的Ribbon攔截
     * 請求的攔截器。
     */

    @Configuration
    @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
   static class LoadBalancerInterceptorConfig {
      @Bean
      public LoadBalancerInterceptor ribbonInterceptor(
            LoadBalancerClient loadBalancerClient,
            LoadBalancerRequestFactory requestFactory)
 
{
         return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
      }

      /**
       * 新增攔截器具體方法。首先獲取當前攔截器集合(List)
       * 然後將loadBalancerInterceptor新增到當前集合中
       * 最後將新的集合放回到restTemplate中。
       */

      @Bean
      @ConditionalOnMissingBean
      public RestTemplateCustomizer restTemplateCustomizer(
            final LoadBalancerInterceptor loadBalancerInterceptor)
 
{
         return new RestTemplateCustomizer() {
            @Override
            public void customize(RestTemplate restTemplate) {
               List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                     restTemplate.getInterceptors());
               list.add(loadBalancerInterceptor);
               restTemplate.setInterceptors(list);
            }
         };
      }
   }
}

至此知道了ribbon攔截請求的基本原理,接下來我們看看Ribbon是怎樣選取server的。

Ribbon選取server原理概覽

通過上面的介紹我們知道了當發起請求時ribbon會用LoadBalancerInterceptor這個攔截器進行攔截。在該攔截器中會呼叫LoadBalancerClient.execute()方法,該方法具體程式碼如下:

@Override
public <T> execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
  /**
   *建立loadBalancer的過程可以理解為組裝選取服務的規則(IRule)、
   *服務叢集的列表(ServerList)、檢驗服務是否存活(IPing)等特性
   *的過程(載入RibbonClientConfiguration這個配置類),需要注意
   *的是這個過程並不是在啟動時進行的,而是當有請求到來時才會處理。
   */

   ILoadBalancer loadBalancer = getLoadBalancer(serviceId);

   /**
    * 根據ILoadBalancer來選取具體的一個Server。
    * 選取的過程是根據IRule、IPing、ServerList
    * 作為參照。
    */

   Server server = getServer(loadBalancer);
   if (server == null) {
      throw new IllegalStateException("No instances available for " + serviceId);
   }
   RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
         serviceId), serverIntrospector(serviceId).getMetadata(server));

   return execute(serviceId, ribbonServer, request);
}

通過程式碼我們可知,首先建立一個ILoadBalancer,這個ILoadBalancer是Ribbon的核心類。可以理解成它包含了選取服務的規則(IRule)、服務叢集的列表(ServerList)、檢驗服務是否存活(IPing)等特性,同時它也具有了根據這些特性從服務叢集中選取具體一個服務的能力。 Server server = getServer(loadBalancer);這行程式碼就是選取舉一個具體server。 最終呼叫了內部的execute方法,該方法程式碼如下(只保留了核心程式碼):

@Override
public <T> execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
   try {
      //發起呼叫
      T returnVal = request.apply(serviceInstance);
      statsRecorder.recordStats(returnVal);
      return returnVal;
   }
   catch (IOException ex) {
      statsRecorder.recordStats(ex);
      throw ex;
   }
   catch (Exception ex) {
      statsRecorder.recordStats(ex);
      ReflectionUtils.rethrowRuntimeException(ex);
   }
   return null;
}

接下來看下request.apply(serviceInstance)方法的具體做了那些事情(LoadBalancerRequestFactory中):

@Override
public ClientHttpResponse apply(final ServiceInstance instance)
      throws Exception 
{
   HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, loadBalancer);
   //省略部分程式碼...
   /**
    * 發起真正請求。
    */

   return execution.execute(serviceRequest, body);
}

看到這裡整體流程的原理就說完了,接下來我們結合一張圖來回顧下整個過程:

img
img

首先獲取所有標識@LoadBalanced註解的RestTemplate(可以理解成獲取那些開啟了Ribbon負載均衡功能的RestTemplate),然後將Ribbon預設的攔截器LoadBalancerInterceptor新增到RestTemplate中,這樣當使用RestTemplate發起http請求時就會起到攔截的作用。當有請求發起時,ribbon預設的攔截器首先會建立ILoadBalancer(裡面包含了選取服務的規則(IRule)、服務叢集的列表(ServerList)、檢驗服務是否存活(IPing)等特性)。在程式碼層面的含義是載入RibbonClientConfiguration配置類)。然後使用ILoadBalancer從服務叢集中選擇一個服務,最後向這個服務傳送請求。

Ribbon負載均衡規則

參考資料:https://www.jianshu.com/p/79b9cf0d0519

Ribbon預設負載均衡規則

根據上述Ribbon的原理,可以知道IRule介面負責負載均衡的實現,具體如下:

image-20210105193640996
image-20210105193640996
規則名稱 特點
AvailabilityFilteringRule 過濾掉一直連線失敗的被標記為circuit tripped的後端Server,並 過濾掉那些高併發的後端Server或者使用一個AvailabilityPredicate 來包含過濾server的邏輯,其實就是檢查status裡記錄的各個server 的執行狀態
BestAvailableRule 選擇一個最小的併發請求的server,逐個考察server, 如果Server被tripped了,則跳過
RandomRule 隨機選擇一個Server
ResponseTimeWeightedRule 已廢棄,作用同WeightedResponseTimeRule
WeightedResponseTimeRule 權重根據響應時間加權,響應時間越長,權重越小,被選中的可能性越低
RetryRule 對選定的負載均衡策略加上重試機制,在一個配置時間段內當 選擇Server不成功,則一直嘗試使用subRule的方式選擇一個 可用的Server
RoundRobinRule 輪詢選擇,輪詢index,選擇index對應位置的Server
ZoneAvoidanceRule 預設的負載均衡策略,即複合判斷Server所在區域的效能和Server的可用性 選擇Server,在沒有區域的環境下,類似於輪詢(RandomRule)

其中RandomRule表示隨機策略、RoundRobinRule表示輪詢策略、WeightedResponseTimeRule表示加權策略、BestAvailableRule表示請求數最少策略等等

隨機原始碼:

image-20210105194052894
image-20210105194052894

輪詢原始碼:

image-20210105194240761
image-20210105194240761

修改預設的自定義規則

預設是輪詢 可以修改為任意的規則

修改為隨機演算法

  1. 建立具有負載均衡功能的RestTemplate例項

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    使用RestTemplate進行rest操作的時候,會自動使用負載均衡策略,它內部會在RestTemplate中加入LoadBalancerInterceptor這個攔截器,這個攔截器的作用就是使用負載均衡。

    預設情況下會採用輪詢策略,如果希望採用其它策略,則指定IRule實現,如:

    @Bean
    public IRule ribbonRule() {
        return new BestAvailableRule();
    }

    這種方式對OpenFeign也有效。

修改為按照Nacos配置的權重進行負載均衡

  1. 在nacos中對叢集進行權重的配置

    image-20210106152111687
    image-20210106152111687
    image-20210106152129819
    image-20210106152129819
    image-20210106152146617
    image-20210106152146617
  2. 在專案中,選擇使用 NacosRule

    image-20210106152259628
    image-20210106152259628

Ribbon實戰優化

飢餓載入

Ribbon預設懶載入,意味著只有在發起呼叫的時候才會建立客戶端

ribbon:
  eager-load:
    # 開啟ribbon飢餓載入
    enabled: true
    # 配置user-center使用ribbon飢餓載入,多個使用逗號分隔
    clients: user-center

引數調優

主要調整請求的超時時間,是否重試

如果業務沒有做冪等性的話建議把重試關掉:ribbon.MaxAutoRetriesNextServer=0

# 從註冊中心重新整理servelist的時間 預設30秒,單位ms
ribbon.ServerListRefreshInterval=15000
# 請求連線的超時時間 預設1秒,單位ms
ribbon.ConnectTimeout=30000
# 請求處理的超時時間 預設1秒,單位ms
ribbon.ReadTimeout=30000
# 對所有操作請求都進行重試,不配置這個MaxAutoRetries不起作用 預設false
#ribbon.OkToRetryOnAllOperations=true
# 對當前例項的重試次數 預設0
# ribbon.MaxAutoRetries=1
# 切換例項的重試次數 預設1
ribbon.MaxAutoRetriesNextServer=0

如果MaxAutoRetries=1MaxAutoRetriesNextServer=1請求在1s內響應,超過1秒先同一個伺服器上重試1次,如果還是超時或失敗,向其他服務上請求重試1次。

那麼整個ribbon請求過程的超時時間為:ribbonTimeout = (ribbonReadTimeout + ribbonConnectTimeout) * (maxAutoRetries + 1) * (maxAutoRetriesNextServer + 1)

如果你覺得這篇內容對你挺有有幫助的話:

  1. 點贊支援下吧,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-)

  2. 歡迎在留言區與我分享你的想法,也歡迎你在留言區記錄你的思考過程。

  3. 覺得不錯的話,也可以關注 程式設計鹿 的個人公眾號看更多文章和講解視訊(感謝大家的鼓勵與支援???)

相關文章