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