[TOC]
*****
# 1. 傳統的負載均衡方式
```
1-1. 服務端負載均衡
1-2. 客戶端負載均衡
```
**1-1 服務端負載均衡**
```
ngnix是部署在服務端的,故稱為服務端負載均衡
```

**1-2 客戶端負載均衡**
```
在內容中心中獲取用戶中心的實例,在內容中心中定義負載均衡的規則,故稱為客戶端負載均衡
```

# 2. 手寫一個客戶端負載均衡器
```
目標: 隨機選擇實例
思路:
1. 通過nacos獲取服務端實例列表
2. 通過算法隨機選擇
3. 代碼如下:
//獲取nacos上ali-pay-service所有的實例
List<ServiceInstance> instances = discooveryClient.getInstances("ali-pay-service");
//獲取請求地址
List<String> targetURLS = instances.stream().map(instance -> instance.getUri().toString()
+ "/users/{id}").collect(Collectors.toList());
//寫隨機算法,獲取隨機下標
int i = ThreadLocalRandom.current().nextInt(targetURLS.size());
//使用restTemplate請求
restTemplate.getForObject(targetURLS.get(i), null);
```
# 3. Ribbon實現負載均衡
**3-1 什么是Ribbon?**
```
Ribbon是Netflix發布的開源項目,主要功能是提供客戶端的軟件負載均衡算法,將Netflix的中間層服務連接在一起.
Ribbon客戶端組件提供一系列完善的配置項如連接超時,重試等.
Ribbon負載均衡主要是輪詢算法,分為以下幾步:
1.根據服務別名,從eureka / nacos獲取服務提供者的客戶端列表
2.將列表緩存到本地,即消費者客戶端的jvm中
3.獲取提供者客戶端下標(總請求數%服務提供者數), 得到調用的服務客戶端的實際地址
```

**3-2 Ribbon實現負載均衡**
```
1. 添加Ribbon依賴
2. 在RestTemplate定義的bean上添加注解 @LoadBalanced
3. 添加配置(可以默認配置)
4. 修改代碼 restTemplate.getForObject("http://ali-pay-service/users/{userId}", null);
```
**3-3 Ribbon的組成**

**3-4 Ribbon內置的負載均衡規則**

**3-5 細粒度配置自定義**
**01-Java代碼方式**
```
/**
* 修改Ribbon負載均衡規則
* 注: 此類要建在啟動類之外
* 父子上下文:
* 1. 啟動類上有@ComponentScan掃描注解,默認會掃描當前類及下屬所有包中相關注解(父上下文)
* 2. Ribbon規則配置有@Configuration注解為子上下文,若在父包下,會出現父子上下文重疊
* 會導致事務不生效,重點: 因此配置ribbon規則時,不要讓RibbonConfiguration被@ComponentScan掃描到
*
*/
@Configuration
public class RibbonConfiguration {
@Bean
public IRule ribbonRule() {
return new RandomRule();
}
}
```
```
/**
* java代碼修改ribbon的負載均衡規則
*/
@Configuration
@RibbonClient(name = "ali-pay-service", configuration = RibbonConfiguration.class)
public class UerCenterRibbonConfiguration {
}
```
**02-配置屬性方式**
```
ali-pay-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
```
**03-倆種配置方式對比**
```
[配置屬性的方式] 比 [代碼配置方式] 的優先級更高!!!
```

**04-ribbon負載均衡規則全局配置**
```
/**
* java代碼修改ribbon負載均衡規則全局配置
* RibbonConfiguration 類就可以放在啟動類下被@ComponentScan掃描到
*/
@Configuration
@RibbonClients(defaultConfiguration = RibbonConfiguration.class)
public class UerCenterRibbonConfiguration {
}
```
**05-ribbon饑餓加載**
```
Ribbon默認是懶加載的, 就是當restTemplate.getForObject("http://ali-pay-service/users/{userId}", null);
這段代碼被調用時,才會加載,因此會導致首次請求過慢的問題.
```
```
解決方案: 通過饑餓加載解決, 添加配置如下:
#饑餓加載配置
ribbon:
eager-load:
enabled: true #開啟饑餓加載
clients: ali-pay-service #為哪些名稱的client開啟,多個用逗號分隔
```
**3-6 擴展Ribbon**
```
3-6-1. 擴展Ribbon-支持Nacos權重
取值在0到1之間, 值越大代表這個實例被調用的幾率越大
Ribbon默認負載均衡規則不支持Nacos權重,所以擴展實現負載均衡權重的規則
```
```
/**
* 基于權重的負載均衡算法
*/
public class NacosWeightedRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
//讀取配置文件并初始化
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
@Override
public Server choose(Object key) {
try {
ILoadBalancer loadBalancer = this.getLoadBalancer();
BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer)loadBalancer;
//想要請求的微服務的名稱
String name = baseLoadBalancer.getName();
//實現負載均衡的算法, 借助nacos已有的算法, 拿到服務發現相關的API
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
//nacos client 自動通過基于權重的負載均衡算法,給我們選擇一個實例
Instance instance = namingService.selectOneHealthyInstance(name);
return new NacosServer(instance);
} catch (NacosException e) {
return null;
}
}
}
```

```
3-6-2. 擴展Ribbon-同一集群優先調用(同機房優先調用) 基于0.9.0版本,官方可能在下個版本加入此功能
public class NacosSameClusterWeightRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
@Override
public Server choose(Object key) {
try {
//拿到配置文件中的集群名稱(DL) spring.cloud.nacos.discovery.cluster-name=DL
String clusterName = nacosDiscoveryProperties.getClusterName();
ILoadBalancer loadBalancer = this.getLoadBalancer();
BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer)loadBalancer;
//想要請求的微服務的名稱
String name = baseLoadBalancer.getName();
//實現負載均衡的算法, 借助nacos已有的算法, 拿到服務發現相關的API
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
// 1. 找到指定服務的所有實例(true代表健康的實例) -- A
List<Instance> instances = namingService.selectInstances(name, true);
// 2. 過濾出相同集群下的所有實例 -- B
List<Instance> sameClusterInstances = instances.stream().filter(instance ->
Objects.equals(instance.getClusterName(), clusterName))
.collect(Collectors.toList());
// 3. 如果B是空就用A
List<Instance> instancesToBeChosen = Lists.newArrayList();
if (CollectionUtils.isEmpty(sameClusterInstances)) {
instancesToBeChosen = instances; //發生跨集群的調用
} else {
instancesToBeChosen = sameClusterInstances;
}
// 4. 基于權重的負載均衡算法,返回1個實例
Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToBeChosen);
return new NacosServer(instance);
} catch (NacosException e) {
return null;
}
}
}
class ExtendBalancer extends Balancer {
public static Instance getHostByRandomWeight2(List<Instance> hosts) {
return getHostByRandomWeight(hosts);
}
}
```
```
3-6-3. 擴展Ribbon-基于元數據的版本控制
場景: 服務間調用可能存在版本控制,如 服務A 的 V1版本必須調用服務B 的 V1版本, 服務A的V2版本必須調用服務B的V2版本
實現微服務之間的版本控制
代碼參考: https://gitee.com/itmuch/spring-cloud-study/tree/master/2019-Spring-Cloud-Alibaba/microservice-consumer-movie-ribbon-rule-with-nacos-2
```
**元數據就是一堆的描述信息,以map存儲。舉個例子:**
```
spring:
cloud:
nacos:
metadata:
# 自己這個實例的版本
version: v1
# 允許調用的提供者版本
target-version: v1
```
**需求分析:**
```
我們需要實現的有兩點:
1. 優先選擇同集群下,符合metadata的實例
2. 如果同集群加沒有符合metadata的實例,就選擇所有集群下,符合metadata的實例
```
```
@Slf4j
public class NacosFinalRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@Override
public Server choose(Object key) {
// 負載均衡規則:優先選擇同集群下,符合metadata的實例
// 如果沒有,就選擇所有集群下,符合metadata的實例
// 1. 查詢所有實例 A
// 2. 篩選元數據匹配的實例 B
// 3. 篩選出同cluster下元數據匹配的實例 C
// 4. 如果C為空,就用B
// 5. 隨機選擇實例
try {
String clusterName = this.nacosDiscoveryProperties.getClusterName();
String targetVersion = this.nacosDiscoveryProperties.getMetadata().get("target-version");
DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
String name = loadBalancer.getName();
NamingService namingService = this.nacosDiscoveryProperties.namingServiceInstance();
// 所有實例
List<Instance> instances = namingService.selectInstances(name, true);
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;
}
}
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);
}
}
Instance instance = ExtendBalancer.getHostByRandomWeight2(clusterMetadataMatchInstances);
return new NacosServer(instance);
} catch (Exception e) {
log.warn("發生異常", e);
return null;
}
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
}
/**負載均衡算法**/
public class ExtendBalancer extends Balancer {
/**
* 根據權重,隨機選擇實例
*
* @param instances 實例列表
* @return 選擇的實例
*/
public static Instance getHostByRandomWeight2(List<Instance> instances) {
return getHostByRandomWeight(instances);
}
}
```
```
3-6-4. 深入理解Nacos的Namespace
Nacos通過Namespace做了環境隔離,只能調用相同Namespace下的實例,而不能跨Namespace調用.
```