[TOC]
*****
# 1. 传统的负载均衡方式
```
1-1. 服务端负载均衡
1-2. 客户端负载均衡
```
**1-1 服务端负载均衡**
```
ngnix是部署在服务端的,故称为服务端负载均衡
```
![](https://img.kancloud.cn/e9/21/e9213e60fdea046cdaafafebd08368fe_548x321.png)
**1-2 客户端负载均衡**
```
在内容中心中获取用户中心的实例,在内容中心中定义负载均衡的规则,故称为客户端负载均衡
```
![](https://img.kancloud.cn/f8/79/f879e8b1a2bc9b01b435bdeb6b4f0926_701x439.png)
# 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.获取提供者客户端下标(总请求数%服务提供者数), 得到调用的服务客户端的实际地址
```
![](https://img.kancloud.cn/42/76/4276d1e51432482a42245ea23f765c47_740x357.png)
**3-2 Ribbon实现负载均衡**
```
1. 添加Ribbon依赖
2. 在RestTemplate定义的bean上添加注解 @LoadBalanced
3. 添加配置(可以默认配置)
4. 修改代码 restTemplate.getForObject("http://ali-pay-service/users/{userId}", null);
```
**3-3 Ribbon的组成**
![](https://img.kancloud.cn/98/5e/985eb6ffd9d5ae60f88f91a23a891e5a_887x401.png)
**3-4 Ribbon内置的负载均衡规则**
![](https://img.kancloud.cn/b0/a3/b0a34e51ee967d144552093188c1a80b_860x496.png)
**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-俩种配置方式对比**
```
[配置属性的方式] 比 [代码配置方式] 的优先级更高!!!
```
![](https://img.kancloud.cn/f3/ce/f3ce24e46ee544f7cf5c35be16174029_795x302.png)
**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;
}
}
}
```
![](https://img.kancloud.cn/8c/ef/8cef9b6e9f37d70214aa7f3b2d84f8b6_826x459.png)
```
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调用.
```