目录
3. 为什么要学习 Spring Cloud Balancer ?
4. Spring Cloud LoadBalancer 内置的两种负载均衡策略
7. Spring Cloud LoadBalancer 中的缓存机制
7.1 Spring Cloud LoadBalancer 中缓存机制的一些特性
1. 什么是 LoadBalancer ?
LoadBalancer(负载均衡器)是一种用来分发网络或应用程序流量到多个服务器的技术。它可以防止任何单一服务的过载,通过分散负载来保持整个系统的平稳运行,保证系统的高可用性和可靠性。
2. 负载均衡策略的分类
负载均衡策略大体上分为两类:服务端的负载均衡和客户端的负载均衡
① 服务端负载均衡 (如 Nginx、F5)
请求先到达一个中介(如负载均衡器设备或者服务,例如Nginx),由这个中介根据配置的策略将请求分发到后端的多个服务器中。它对客户端是透明的,即客户端不需要知道有多少服务器以及它们的存在。
② 客户端负载均衡 (如 Ribbon、Spring Cloud LoadBalancer)
请求的分配逻辑由客户端持有,客户端直接决定将请求发送到哪一个服务器。也就是说在客户端负载均衡中,客户端通常具备一份服务列表,它知道每个服务的健康状况,基于这些信息和负载均衡策略,客户端会选择一个最适合的服务去发送请求。
服务端负载均衡和客户端负载均衡策略有什么区别 ?
它俩的区别主要在灵活性和性能两方面(结合上面两幅图来理解):
1. 灵活性
- 客户端负载均衡更加灵活,它可以针对每一个请求,每一个 service 做单独的负载均衡配置。
2. 性能
- 客户端负载均衡性能相对来说更好一点,因为服务端负载均衡中,当请求来了之后,它得先去到服务端的负载均衡,然后服务端的负载均衡再将请求发送给对应的服务器,整个过程发送了两次请求,而客户端负载均衡只需要发送一次请求。
- 其次,服务端负载均衡中,客户端的请求都先打到了中心节点上,这个流量是很大的,所以服务端的负载均衡器,它的压力相对来说就比较大,那么性能就不可能比客户端负载均衡高。
- 反观客户端负载均衡,它就没有所谓的中心节点,它将集中的压力给释放了,因为客户端有成千上万个,它可以让每个客户端去调用自己的负载均衡器,而不是让成千上万个客户端去调用一个负载均衡器。
【扩充知识】
如果将负载均衡器视为代理,那么服务端负载均衡可以视作是反向代理的一种形式,因为它接收客户端请求后再决定将其分配给哪一个服务器;而客户端负载均衡则可以看作具有正向代理的性质,因为客户端知道要联系的服务列表,并直接向选定的服务器发送请求。
正向代理:正向代理类似于一个中间人,代表客户端去请求服务。客户端必须要配置代理,因此客户端是知道代理的存在的。正向代理隐藏了客户端的信息,服务器不知道真正的请求者是谁。
反向代理:反向代理则是代表服务器接收客户端的请求。客户端通常不知道后面有多少服务器,也不需要知道。反向代理隐藏了服务端的信息,客户端只与反向代理交互,像Nginx这样的服务器就是一个反向代理的例子。
2.1 常见的负载均衡策略
常见的负载均衡策略有以下几种:
- 轮询:按顺序分配,每个服务器轮流接收一个连接。
- 随机选择:随机挑选服务器,分散负载。
- 最少连接:选择当前连接数最少的服务器。
- IP哈希:根据用户IP分配,相同IP的请求总是发给同一服务器。
- 加权轮询:类似轮询,但服务器根据权重获取更多或更少请求。
- 加权随机选择:权重高的服务器有更高几率被选中。
- 最短响应时间:响应时间短的服务器优先接收新请求。
3. 为什么要学习 Spring Cloud Balancer ?
因为 Ribbon 作为早期的客户端负载均衡工具,在 Spring Cloud 2020.0.0 版本之后已经被移除了,取而代之的是 Spring Cloud LoadBalancer,而且 Ribbon 也已经不再维护,所以它也是 Spring 官方推荐的负载均衡解决方案。
其他一些原因:
更好的兼容性:LoadBalancer就像一个全新的配件,它与Spring Cloud的其他组件搭配得更好。
支持响应式编程:现在编程界有一种新的编程方式叫做“响应式编程”,LoadBalancer能很好地支持这种现代编程风格。
易于使用和维护:LoadBalancer的设计易于拼装和修改,这对于开发者来说,维护和定制起来更加方便。
多功能:LoadBalancer有很多内置功能,比如自动帮你挑选服务器,就像购物网站帮你推荐商品一样聪明。
4. Spring Cloud LoadBalancer 内置的两种负载均衡策略
4.1 轮询负载均衡策略(默认的)
从它的源码实现可以看出来默认的负载均衡策略是轮询的策略。
IDEA 搜索它的配置类 LoadBalancerClientConfiguration:
进入到 RoundRobinLoadBalancer 这个类里边,定位到 getInstanceResponse 方法,就能看到轮询策略的关键代码:
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);
}
}
理解关键代码:
int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
this.position.
incrementAndGet()
方法等价于 "++随机数 "。这是一个原子操作,保证了每次调用都会得到一个唯一的递增数值。& Integer.MAX_VALUE
这部分是一个位运算,它确保了如果position
的值增加到超过Integer.MAX_VALUE
时,不会产生负数。其一,在轮询算法中,如果计数器变成负数,那么取余操作可能会产生负的索引值,这是无效的; 其二,也可也保证在相同规则底下的公平性。
ServiceInstance instance = (ServiceInstance)
instances.get(pos % instances.size()); // 进行轮询选择
instances
是一个包含所有服务实例的列表。pos % instances.size()
计算的是pos
除以instances
列表大小的余数,这保证了不论pos
增长到多大,这个表达式的结果都是在0
到instances.size() - 1
的范围内,这样就可以循环地从服务实例列表中选择服务实例。
4.2 随机负载均衡策略
实现随机负载均衡策略的步骤:
① 创建随机负载均衡策略
② 设置随机负载均衡策略
接下来的操作都是基于这篇博客基础上去操作的,有需要的可以先去看看这篇博客,先把前置的代码准备好:https://blog.csdn.net/xaiobit_hl/article/details/134142521
4.2.1 创建随机负载均衡策略
这些写法都是相通的,可以仿照源码中的轮询策略的关键代码:
可以去源码中的LoadBalancerClientConfiguration中去定位到 reactorServiceInstanceLoadBalancer 方法,然后复制下来,修改几个关键地方即可。
public class RandomLoadBalancerConfig {
// 随机的负载均衡策略
@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty("loadbalancer.client.name");
return new RandomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name,
ServiceInstanceListSupplier.class), name);
}
}
4.2.2 设置随机负载均衡策略
在 consumer 模块中的 service 接口上设置负载均衡策略:
@Service
@FeignClient("loadbalancer-service")
// 设置局部的负载均衡策略
@LoadBalancerClient(name = "loadbalancer-service",
configuration = RandomLoadBalancerConfig.class)
public interface UserService {
@RequestMapping("/user/getname")
public String getName(@RequestParam("id") Integer id);
}
PS:有时候局部的负载均衡策略不会生效(版本问题),可以将其设为全局的负载均衡策略。
如何设置全局的负载均衡策略:(在启动类上加 @LoadBalancerClients 注解)
@SpringBootApplication
@EnableFeignClients // 开启 OpenFeign
// 设置全局的负载均衡策略
@LoadBalancerClients(defaultConfiguration =
RandomLoadBalancerConfig.class)
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
这个时候,就是随机的负载均衡策略了,可以启动两个生产者和消费者,然后拿着消费者这边的端口去获取服务感受。
5. Nacos 权重负载均衡器
Nacos 中有两种负载均衡策略:权重负载均衡策略和 CMDB(地域就近访问)标签负载均衡策略
它默认的策略是权重。
在 Spring Cloud Balancer 配置为 Nacos 负载均衡器的步骤:
① 创建 Nacos 负载均衡器
② 设置 Nacos 负载均衡器
5.1 创建 Nacos 负载均衡器
配置 Nacos 负载均衡需要注入 NacosDiscoveryProperties 这个类,因为它需要使用到配置文件中的一些关键信息。
@LoadBalancerClients(defaultConfiguration = NacosLoadBalancerConfig.class)
public class NacosLoadBalancerConfig {
@Resource
NacosDiscoveryProperties nacosDiscoveryProperties;
@Bean
public ReactorLoadBalancer<ServiceInstance> nacosLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty("loadbalancer.client.name");
return new NacosLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name,
ServiceInstanceListSupplier.class), name, nacosDiscoveryProperties);
}
}
5.2 设置 Nacos 负载均衡器
@Service
@FeignClient("loadbalancer-service")
// 设置局部的负载均衡策略
@LoadBalancerClient(name = "loadbalancer-service",
configuration = NacosLoadBalancerConfig.class)
public interface UserService {
@RequestMapping("/user/getname")
public String getName(@RequestParam("id") Integer id);
}
再测试之前,可以先将 Nacos 中一个生产者的权重给设置为 10,一个设置为 1,这样就能明显感受到 Nacos 权重的负载均衡策略了。
6. 自定义负载均衡器
自定义负载均衡策略需要 3 个步骤:
① 创建自定义负载均衡器
② 封装自定义负载均衡器
③ 为服务设置自定义负载均衡策器
6.1 创建自定义负载均衡器
这里也是可以参考源码的实现的,搜索 RandomLoadBalancer 这个类,模仿它的实现去创建自定义负载均衡器。
Ⅰ. 创建一个负载均衡类, 并让其实现 ReactorServiceInstanceLoadBalancer 接口;
Ⅱ. 复制 RandomLoadBalancer 的整个方法体,粘贴到自定义负载均衡类中,并修改构造方法名称
Ⅲ. 在关键方法 getInstanceResponse 中实现自定义负载均衡策略(以IP哈希负载均衡为例)
public class CustomLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final Log log = LogFactory.getLog(CustomLoadBalancer.class);
private final String serviceId;
private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
public CustomLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
}
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> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) {
Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(serviceInstances);
if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
((SelectedInstanceCallback)supplier).selectedServiceInstance((ServiceInstance)serviceInstanceResponse.getServer());
}
return serviceInstanceResponse;
}
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 {
// 自定义负载均衡策略
// 获取 Request 对象
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String ipAddress = request.getRemoteAddr();
System.out.println("用户 IP:" + ipAddress);
int hash = ipAddress.hashCode();
// IP 哈希负载均衡【关键代码】
int index = hash % instances.size();
// 得到服务实例的方法
ServiceInstance instance = (ServiceInstance) instances.get(index);
return new DefaultResponse(instance);
}
}
}
6.2 封装自定义负载均衡器
public class CustomLoadBalancerConfig {
// IP 哈希负载均衡
@Bean
public ReactorLoadBalancer<ServiceInstance> customLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty("loadbalancer.client.name");
return new CustomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name,
ServiceInstanceListSupplier.class), name);
}
}
6.3 为服务设置自定义负载均衡策器
@Service
@FeignClient("loadbalancer-service")
// 设置局部的负载均衡策略
@LoadBalancerClient(name = "loadbalancer-service",
configuration = CustomLoadBalancerConfig.class)
public interface UserService {
@RequestMapping("/user/getname")
public String getName(@RequestParam("id") Integer id);
}
PS:测试的时候发现自定义的负载均衡策略不生效怎么办 ?
① 把前边的 Nacos 的负载均衡器一整个注释掉(包括 @LoadBalancerClients注解),只提供一个类。
② 如果设置局部的负载均衡不生效,就去启动类上设置全局的负载均衡策略。
@SpringBootApplication
@EnableFeignClients // 开启 OpenFeign
// 设置全局的负载均衡策略
@LoadBalancerClients(defaultConfiguration =
CustomLoadBalancerConfig.class)
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
7. Spring Cloud LoadBalancer 中的缓存机制
Spring Cloud LoadBalancer 中获取服务实例有两种方式:
1. 实时获取:每次都从注册中心得到最新的健康实例(效果好,开销大)
2. 缓存服务列表:每次得到服务列表之后,缓存一段时间(既保证性能,也能保证一定的及时性)
Spring Cloud LoadBalancer 默认开启了缓存服务列表的功能。
测试 Spring Cloud LoadBalancer 的缓存机制:
1. 将前面设置负载均衡策略全部注释掉,使用默认的轮询测试(便于观察)
2. 准备两个服务
3. 将其中一个服务下线,下线的同时立马去获取服务,然后等大约 35s ,再去获取服务
【测试结果】 当我下线第一个服务的时候,立马去获取服务,这个时候还是两个服务轮询的获取,等过了 35s 左右,就只能获取到 64067 这个服务了。
7.1 Spring Cloud LoadBalancer 中缓存机制的一些特性
默认特性如下:
① 缓存的过期时间为 35s;
② 缓存保存个数为 256 个。
我们可以通过在配置文件中去设置这些特性:
spring:
cloud:
loadbalancer:
cache:
ttl: 35s # 过期时间
capacity: 1024 # 设置缓存个数
7.2 关闭缓存
关闭 Spring Cloud LoadBalancer 中的缓存可以通过以下配置文件来设置:
spring:
cloud:
loadbalancer:
cache:
enabled: false # 关闭缓存
PS:尽管关闭缓存对于开发和测试很有用,但是在生产环境上,它的效率是要远低于开启缓存,所以在生产环境上始终都要开启缓存。