最近在学习SpringCloud,对于LoadBalancer有所好奇,于是就深挖了一些底层原理。
目录
1. LoadBalancer概述
Spring Cloud LoadBalancer是由SpringCloud官方提供的一个开源的、简单易用的客户端负载均衡器。官方文档
相对于Nginx来说,LoadBalancer的特点在于其客户端负载均衡,它会向注册中心获取注册信息列表后,缓存到JVM中,通过轮询算法选择调用的服务端发送请求。
而Nginx则是服务器端负载均衡,客户端所有的请求都会交给Nginx,由Nginx实现服务端的选择,以及转发请求。
LoadBalancer具体的负载均衡实现逻辑在ReactorServiceInstanceLoadBalancer实现类中,常见的有三个:RoundRobinLoadBalancer,RandomLoadBalancer和NacosLoadBalancer。
ReactorServiceInstanceLoadBalancer的一些通用属性:
serviceId:服务的唯一标识,用于指定当前负载均衡器所要操作的服务。
serviceInstanceListSupplierProvider;提供服务实例列表的供应者,用于在负载均衡时提供该服务的所有可用实例。
2. RoundRobinLoadBalancer(Consul默认)
2.1 依赖版本
不同版本的源码可能有所不同
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>4.0.4</version>
</dependency>
在这个包下面一共有两种负载均衡算法的实现类:RoundRobinLoadBalancer和RandomLoadBalancer
2.2 源码
LoadBalancer(consul为注册中心)默认使用的是RoundRobinLoadBalancer类
提取了一些实现负载均衡核心逻辑的代码
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get(request).next().map((serviceInstances) -> {
return this.processInstanceResponse(supplier, serviceInstances);
});
}
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
if (log.isWarnEnabled()) {
log.warn("No servers available for service: " + this.serviceId);
}
return new EmptyResponse();
} else if (instances.size() == 1) {
return new DefaultResponse((ServiceInstance)instances.get(0));
} else {
int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
ServiceInstance instance = (ServiceInstance)instances.get(pos % instances.size());
return new DefaultResponse(instance);
}
}
choose方法:实现了负载均衡的核心逻辑
serviceInstanceListSupplierProvider.getIfAvailable
:获取一个 实例,该实例提供服务实例的列表-->哪些生产者能够提供服务。supplier.get(request).next()
:获取下一个可用的服务实例。map
操作:使用响应式编程的方式,异步通过getInstanceResponse方法处理返回的服务实例列表,选择下一个选择的合适的服务实例(负载均衡)
getInstanceResponse方法:实现了负载均衡的核心算法:轮询算法
- 如果
instances
为空,返回EmptyResponse
。 - 如果
instances
只有一个,直接返回该实例。 - 如果有多个实例:
this.position.incrementAndGet() & Integer.MAX_VALUE
:对position
计数器执行递增操作,然后使用& Integer.MAX_VALUE
来确保值为非负整数。pos % instances.size()
:使用模运算来选择当前轮询到的服务实例
2.3 解析
总结一下:装载了LoadBalancer的消费者客户端会先向服务注册中心注册服务,并获取生产者服务列表,然后会由LoadBalancer通过轮询算法选取合适的提供服务的生产者,并发送请求。
轮询算法:请求数%服务器总集群数=实际调用服务器位置下标。每次服务重启请求数重新从1开始。
例如上图的集群(集群数为3):
第一次请求: 1%3=1 则请求生产者1
第二次请求: 2%3=2 请求生产者2
第三次请求: 3%3=3 请求生产者0
以此类推
3. NacosLoadBalancer(Nacos默认)
如果你是使用Nacos的用户,它自定义了一个NacosLoadBalancer的负载均衡算法,也是其默认算法。
3.1 依赖版本
虽然名为NacosLoadBalancer,但它却是在nacos包下的,和其他两个不同,它不在LoadBalancer依赖中。但是,如果你想要使用负载均衡的话,还是必须得引入LoadBalancer依赖以提供一些其余组件。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2022.0.0.0-RC2</version>
</dependency>
3.2 源码
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get(request).next().map(this::getInstanceResponse);
}
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> serviceInstances) {
if (serviceInstances.isEmpty()) {
log.warn("No servers available for service: " + this.serviceId);
return new EmptyResponse();
} else {
try {
String clusterName = this.nacosDiscoveryProperties.getClusterName();
List<ServiceInstance> instancesToChoose = serviceInstances;
if (StringUtils.isNotBlank(clusterName)) {
List<ServiceInstance> sameClusterInstances = (List)serviceInstances.stream().filter((serviceInstance) -> {
String cluster = (String)serviceInstance.getMetadata().get("nacos.cluster");
return StringUtils.equals(cluster, clusterName);
}).collect(Collectors.toList());
if (!CollectionUtils.isEmpty(sameClusterInstances)) {
instancesToChoose = sameClusterInstances;
}
} else {
log.warn("A cross-cluster call occurs,name = {}, clusterName = {}, instance = {}", new Object[]{this.serviceId, clusterName, serviceInstances});
}
instancesToChoose = this.filterInstanceByIpType(instancesToChoose);
ServiceInstance instance = NacosBalancer.getHostByRandomWeight3(instancesToChoose);
return new DefaultResponse(instance);
} catch (Exception var5) {
Exception e = var5;
log.warn("NacosLoadBalancer error", e);
return null;
}
}
}
主要关注与RoundRobinLoadBalancer不同的实现部分
集群不为空时:
serviceInstances.stream().filter(...)
: 使用流操作过滤出与当前集群名称匹配的服务实例。serviceInstance.getMetadata().get("nacos.cluster")
: 从服务实例的元数据中获取其所属的集群名称。StringUtils.equals(cluster, clusterName)
: 判断实例的集群名称是否与当前集群名称相同。.collect(Collectors.toList())
: 将过滤后的实例收集成列表sameClusterInstances
。.filterInstanceByIpType:
根据IP类型过滤实例
重点:从集群中选择合适服务的方法为:NacosBalancer.getHostByRandomWeight3()
这是一个包装的非常重量级的算法:加权随机算法,这里就不展开了(从方法名可知这个方法被调用的是第三层),感兴趣的朋友们自己学习一下源码吧。
原理:
- 计算服务列表的总权重
- 生成一个【0,总权重)的随机数
- 遍历实例列表,累加实例权重,当累加值首次超过随机数r时,选择当前实例
3.3 解析
负载均衡算法:加权随机算法
总权重: 5 + 3 + 7 =15
因此每次请求时:
请求生产者0的概率为: 15 / 5
请求生产者1的概率为: 15 / 3
请求生产者2的概率为: 15 / 7
若第一次请求,随机数为5 :选择生产者1
若第二次请求,随机数为14 :选择生产者2
若第三次请求,随机数为4 :选择生产者0
4. 修改默认算法
如果我们偶尔不满意于LoadBalancer默认的轮询算法(绝大多数情况下都足够了),也可以使用官方提供的第二种随机算法或者自定义算法。
4.1 随机算法
顾名思义,完全随机地选择服务实例。
按照官方文档复制粘贴即可
结合rest进行远程调用
@Configuration
@LoadBalancerClient(value = "cloud-payment-service",
//不填写value值则默认将负载均衡配置应用于所有服务调用
//value值大小写一定要和想要修改的注册进服务中心的微服务名称必须一样
configuration = RestTemplateConfig.class)
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
结合openFeign进行远程调用
@Configuration
@LoadBalancerClient(value = "cloud-payment-service",
//不填写value值则默认将负载均衡配置应用于所有服务调用
//value值大小写一定要和想要修改的注册进服务中心的微服务名称必须一样
configuration = FeignConfiguration.class)
public class FeignConfiguration {
@Bean
public ReactorServiceInstanceLoadBalancer loadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
4.2 自定义算法
实现该接口(具体我就不实现了)
public class MyLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private final String serviceId;
private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
public MyLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
return null;
}
}
然后修改ReactorServiceInstanceLoadBalancer返回的实现对象即可
return new MyLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
5. 总结
LoadBalancer虽然底层思想大多非常简单,且不经常修改,但是了解其底层原理仍有助于对于负载均衡的使用。