Spring Cloud Alibaba學習筆記(3) – Ribbon

  • 2019 年 10 月 21 日
  • 筆記

1.手寫一個客戶端負載均衡器

  在了解什麼是Ribbon之前,首先通過程式碼的方式手寫一個負載均衡器

RestTemplate restTemplate = new RestTemplate();    // 獲取請求示例  List<ServiceInstance> instances = discoveryClient.getInstances("study02");  List<String> collect = instances.stream()          .map(instance -> instance.getUri().toString() + "/find")          .collect(Collectors.toList());    // 隨機演算法  int i = ThreadLocalRandom.current().nextInt(collect.size());  String targetURL =  collect.get(i);    log.info("請求的目標地址: {}", targetURL);    DemoComment forObject = restTemplate.getForObject(targetURL, DemoComment.class, 1);

 

2.Ribbon是什麼

  Ribbon是Netflix發布的開源項目,主要功能是提供客戶端的軟體負載均衡演算法,將Netflix的中間層服務連接在一起。Ribbon客戶端組件提供一系列完善的配置項如連接超時,重試等。

  簡單來說,Ribbon就是簡化了上面程式碼的組件,其中提供了更多的負載均衡演算法。

 

3.整合Ribbon

  依賴:因為spring-cloud-starter-alibaba-nacos-discovery中已經包含了Ribbon,所以不需要加依賴

  註解:

    在註冊RestTemplate的地方添加註解

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

  配置:沒有配置

  

  配置完成,此時,章節1中的手寫負載均衡器程式碼可以簡化為:

DemoComment forObject = restTemplate.getForObject("http://study02/find", DemoComment.class, 1);

  當restTemplate組織請求的時候,Ribbon會自動把“study02”轉換為該服務在Nacos上面的地址,並且進行負載均衡

 

  PS:默認情況下Ribbon是懶載入的。當服務起動好之後,第一次請求是非常慢的,第二次之後就快很多。

  解決方法:開啟飢餓載入

ribbon:   eager-load:    enabled: true #開啟飢餓載入    clients: server-1,server-2,server-3 #為哪些服務的名稱開啟飢餓載入,多個用逗號分隔

 

4.Ribbon的組成

介面 作用 默認值
IclientConfig 讀取配置 DefaultClientConfigImpl
IRule 負載均衡規則,選擇實例 ZoneAvoidanceRule
IPing 篩選掉ping不通的實例 DumyPing(該類什麼不幹,認為每個實例都可用,都能ping通)
ServerList 交給Ribbon的實例列表 Ribbon:ConfigurationBasedServerList
Spring Cloud Alibaba:NacosServerList
ServerListFilter 過濾掉不符合條件的實例 ZonePreferenceServerListFilter
ILoadBalancer Ribbon的入口 ZoneAwareLoadBalancer
ServerListUpdater 更新交給Ribbon的List的策略 PollingServerListUpdater

 

4.1 Ribbon內置的負載均衡規則

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

4.2 細粒度配置

  示例:調用服務A採用默認的負載均衡規則ZoneAvoidanceRule,調用服務B採用隨即RandomRule規則

4.2.1 Java程式碼

  新建一個ribbonConfiguration包(一定要在啟動類所在的包以外),ribbonConfiguration包內新建RibbonConfiguration類

  ps:為什麼ribbonConfiguration包一定要在啟動類所在的包以外,RibbonConfiguration類引用了@Configuration註解,這個註解組合了@Component註解,可以理解為@Configuration是一個特殊的@Component。

  而在啟動類中有一個@SpringBootApplication註解,其中組合了@ComponentScan註解,這個註解是用來掃貓@Component的,包括@Configuration註解,掃貓的分為當前啟動類所在的包以及啟動類所在包下面的所有的@Component。

  而Spring的上下文是樹狀的上下文,@SpringBootApplication所掃貓的上下文是主上下文;而ribbon也會有一個上下文,是子上下文。而父子上下文掃貓的包一旦重疊,會導致很多問題【在本文中會導致配置被共享】,所以ribbon的配置類一定不能被啟動類掃貓到,ribbonConfiguration包一定要在啟動類所在的包以外。

package ribbonConfiguration;    import com.netflix.loadbalancer.IRule;  import com.netflix.loadbalancer.RandomRule;  import org.springframework.context.annotation.Bean;  import org.springframework.context.annotation.Configuration;    /**   * java程式碼實現細粒度配置   * 注意是單獨的包   * 父子上下文重複掃貓問題   */  @Configuration  public class RibbonConfiguration {      @Bean      public IRule ribbonRule() {          return new RandomRule();      }  }

  新建DemoRibbonConfiguration類,為服務B指定負載均衡演算法

import org.springframework.cloud.netflix.ribbon.RibbonClient;  import org.springframework.cloud.netflix.ribbon.RibbonClients;  import org.springframework.context.annotation.Configuration;  import ribbonConfiguration.RibbonConfiguration;    /**   * 自定義負載均衡方式   */  @Configuration  // 單個配置  @RibbonClient(name = "B", configuration = RibbonConfiguration.class)  public class DemoRibbonConfiguration {    }

4.2.2 配置屬性方式

# ribbon 配置方式實現細粒度配置  demo-center:    ribbon:      NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

  其他參數:

     NFLoadBalancerClassName: #ILoadBalancer該介面實現類
     NFLoadBalancerRuleClassName: #IRule該介面實現類
     NFLoadBalancerPingClassName: #Iping該介面實現類
     NIWSServerListClassName: #ServerList該介面實現類
     NIWSServerListFilterClassName: #ServiceListFilter該介面實現類

 

  優先順序:屬性配置 > 程式碼配置

  盡量使用屬性配置,屬性配置解決不了問題的時候在考慮使用程式碼配置

  在同一個微服務內盡量保證配置單一性  

 

4.3 全局配置

import org.springframework.cloud.netflix.ribbon.RibbonClient;  import org.springframework.cloud.netflix.ribbon.RibbonClients;  import org.springframework.context.annotation.Configuration;  import ribbonConfiguration.RibbonConfiguration;    @Configuration  // 全局配置  @RibbonClients(defaultConfiguration = RibbonConfiguration.class)  public class DemoRibbonConfiguration {    }

  還有一種方法,4.2.1中提到的,讓ComponentScan上下文重疊,從而實現全局配置,不建議使用這種方法。

 

5.Ribbon擴展

5.1 支援Nacos權重

  在Nacos的控制台,可以為每一個實例配置權重,取值在0~1之間,值越大,表示這個實例被調用的幾率越大。而Ribbon內置的負載均衡的規則不支援權重,我們可以通過程式碼的方式讓ribbon支援Nacos權重。

import com.alibaba.cloud.nacos.NacosDiscoveryProperties;  import com.alibaba.cloud.nacos.ribbon.NacosServer;  import com.alibaba.nacos.api.exception.NacosException;  import com.alibaba.nacos.api.naming.NamingService;  import com.alibaba.nacos.api.naming.pojo.Instance;  import com.netflix.client.config.IClientConfig;  import com.netflix.loadbalancer.AbstractLoadBalancerRule;  import com.netflix.loadbalancer.BaseLoadBalancer;  import com.netflix.loadbalancer.Server;  import org.springframework.beans.factory.annotation.Autowired;    /**   * 基於Nacos 權重的負載均衡演算法   */  public class NacosWeightRule extends AbstractLoadBalancerRule {        @Autowired      private NacosDiscoveryProperties nacosDiscoveryProperties;        @Override      public void initWithNiwsConfig(IClientConfig iClientConfig) {          // 讀取配置文件,並初始化NacosWeightRule      }        @Override      public Server choose(Object o) {          try {              // ribbon入口              BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();                //想要請求的微服務的名稱              String name = loadBalancer.getName();                // 實現負載均衡演算法              // 拿到服務發現的相關api              NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();              // nacos client自動通過基於權重的負載均衡演算法,給我們一個示例              Instance instance = namingService.selectOneHealthyInstance(name);                return new NacosServer(instance);          } catch (NacosException e) {              return null;          }      }  }

 

5.2 同一集群優先調用

import com.alibaba.cloud.nacos.NacosDiscoveryProperties;  import com.alibaba.cloud.nacos.ribbon.NacosServer;  import com.alibaba.nacos.api.exception.NacosException;  import com.alibaba.nacos.api.naming.NamingService;  import com.alibaba.nacos.api.naming.pojo.Instance;  import com.alibaba.nacos.client.naming.core.Balancer;  import com.netflix.client.config.IClientConfig;  import com.netflix.loadbalancer.AbstractLoadBalancerRule;  import com.netflix.loadbalancer.BaseLoadBalancer;  import com.netflix.loadbalancer.Server;  import lombok.extern.slf4j.Slf4j;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.util.CollectionUtils;    import java.util.ArrayList;  import java.util.List;  import java.util.Objects;  import java.util.stream.Collectors;    /**   * 同一集群優先調用   */  @Slf4j  public class NacosSameClusterWeightedRule extends AbstractLoadBalancerRule {        @Autowired      private NacosDiscoveryProperties nacosDiscoveryProperties;        @Override      public void initWithNiwsConfig(IClientConfig iClientConfig) {        }        @Override      public Server choose(Object o) {          try {              // 拿到配置文件中的集群名稱              String clusterName = nacosDiscoveryProperties.getClusterName();                BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();              String name = loadBalancer.getName();              NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();              // 找到指定服務的所有示例 A              List<Instance> instances = namingService.selectInstances(name, true);              // 過濾出相同集群的所有示例 B              List<Instance> sameClusterInstances = instances.stream()                      .filter(instance -> Objects.equals(instance.getClusterName(), clusterName))                      .collect(Collectors.toList());              // 如果B是空,就用A              List<Instance> instancesToBeanChoose = new ArrayList<>();              if (CollectionUtils.isEmpty(sameClusterInstances)) {                  instancesToBeanChoose = instances;              } else {                  instancesToBeanChoose = sameClusterInstances;              }              // 基於權重的負載均衡演算法返回示例              Instance hostByRandomWeightExtend = ExtendBalancer.getHostByRandomWeightExtend(instancesToBeanChoose);                return new NacosServer(hostByRandomWeightExtend);          } catch (NacosException e) {              log.error("發生異常了", e);              return null;          }      }  }    class ExtendBalancer extends Balancer {      public static Instance getHostByRandomWeightExtend(List<Instance> hosts) {          return getHostByRandomWeight(hosts);      }  }

 

5.3 基於元數據的版本控制

import com.alibaba.cloud.nacos.NacosDiscoveryProperties;  import com.alibaba.cloud.nacos.ribbon.NacosServer;  import com.alibaba.nacos.api.naming.NamingService;  import com.alibaba.nacos.api.naming.pojo.Instance;  import com.alibaba.nacos.client.naming.utils.CollectionUtils;  import com.netflix.client.config.IClientConfig;  import com.netflix.loadbalancer.AbstractLoadBalancerRule;  import com.netflix.loadbalancer.BaseLoadBalancer;  import com.netflix.loadbalancer.Server;  import lombok.extern.slf4j.Slf4j;  import org.apache.commons.lang.StringUtils;  import org.springframework.beans.factory.annotation.Autowired;    import java.util.List;  import java.util.Objects;  import java.util.stream.Collectors;    /**   * 基於nacos元數據的版本控制   */  @Slf4j  public class NacosFinalRule extends AbstractLoadBalancerRule {      @Autowired      private NacosDiscoveryProperties nacosDiscoveryProperties;        @Override      public void initWithNiwsConfig(IClientConfig iClientConfig) {        }        @Override      public Server choose(Object o) {          try {              // 負載均衡規則:優先選擇同集群下,符合metadata的實例              // 如果沒有,就選擇所有集群下,符合metadata的實例                // 1. 查詢所有實例 A              String clusterName = nacosDiscoveryProperties.getClusterName();              String targetVersion = this.nacosDiscoveryProperties.getMetadata().get("target-version");                BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();              String name = loadBalancer.getName();              NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();              List<Instance> instances = namingService.selectInstances(name, true);                // 2. 篩選元數據匹配的實例 B              List<Instance> metadataMatchInstances = instances;              // 如果配置了版本映射,那麼只調用元數據匹配的實例              if (StringUtils.isNotBlank(targetVersion)) {                  metadataMatchInstances = instances.stream()                          .filter(instance -> Objects.equals(targetVersion, instance.getMetadata().get("version")))                          .collect(Collectors.toList());                  if (CollectionUtils.isEmpty(metadataMatchInstances)) {                      log.warn("未找到元數據匹配的目標實例!請檢查配置。targetVersion = {}, instance = {}", targetVersion, instances);                      return null;                  }              }                // 3. 篩選出同cluster下元數據匹配的實例 C              // 4. 如果C為空,就用B              List<Instance> clusterMetadataMatchInstances = metadataMatchInstances;              // 如果配置了集群名稱,需篩選同集群下元數據匹配的實例              if (StringUtils.isNotBlank(clusterName)) {                  clusterMetadataMatchInstances = metadataMatchInstances.stream()                          .filter(instance -> Objects.equals(clusterName, instance.getClusterName()))                          .collect(Collectors.toList());                  if (CollectionUtils.isEmpty(clusterMetadataMatchInstances)) {                      clusterMetadataMatchInstances = metadataMatchInstances;                      log.warn("發生跨集群調用。clusterName = {}, targetVersion = {}, clusterMetadataMatchInstances = {}", clusterName, targetVersion, clusterMetadataMatchInstances);                  }              }                // 5. 隨機選擇實例              Instance instance = ExtendBalancer.getHostByRandomWeightExtend(clusterMetadataMatchInstances);              return new NacosServer(instance);          } catch (Exception e) {              return null;          }      }  }