1. 程式人生 > >Spring Cloud Alibaba學習筆記(3) - Ribbon

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

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;
        }
    }
}

&n