负载均衡 -LoadBalance

目录

问题分析

负载均衡

服务端负载均衡

客户端负载均衡

Spring Cloud LoadBalancer

自定义负载均衡策略

实现原理


问题分析

在 服务注册与发现——Eureka-ZEEKLOG博客 中,我们根据应用名称获取了服务实例列表,并从列表中选择了一个服务实例:

若一个服务对应多个实例,是否能够将流量合理的分配到多个实例呢?

我们启动多个 product-service 实例

修改端口号:

再添加一个实例,并启动:

观察 Eureka,可以看到 product-service 中有三个实例:

此时,我们多次访问 127.0.0.1:8080/order/1

可以看到,多次访问的都是同一台机器,我们启动多个实例,就是希望能够减轻单机压力,也就是每个实例处理部分请求,而不是让同一台机器处理所有请求

那么,如何实现多个机器分摊负荷呢?

我们可以依次将请求分发给服务器列表中的每一台机器,因此,我们对 OrderService 中代码进行修改:

@Slf4j @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; // 当前选择实例 private static AtomicInteger atomicInteger = new AtomicInteger(1); // 实例列表 private static List<ServiceInstance> instances; @PostConstruct public void init() { // 获取服务列表 instances = discoveryClient.getInstances("product-service"); } public OrderInfo findOrderInfoById(Integer orderId) { OrderInfo orderInfo = orderMapper.selectOrderById(orderId); // 计算当前访问实例 int index = atomicInteger.getAndIncrement() % instances.size(); EurekaServiceInstance instance = (EurekaServiceInstance)instances.get(index); log.info("选择实例: " + instance.getInstanceId()); // 拼接 URL String url = instance.getUri() + "/product/" + orderInfo.getProductId(); ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class); orderInfo.setProductInfo(productInfo); return orderInfo; } }

 多次访问 127.0.0.1:8080/order/1

此时请求被均衡地分配到了不同实例上,上述这种方式,就是负载均衡

负载均衡

负载均衡(Load Balance,简称 LB)是高并发、高可用系统必不可少的关键组件,目的是将网络流量或计算任务智能地分配到多个服务器(或资源)从而 提高系统性能(响应更快)、增强可用性与可靠性(一台服务器宕机,其他还能继续服务)、提升可扩展性(通过增加服务器来应对更多请求)以及 避免单点过载(防止某台服务器被压垮)

我们通过一个生活中的示例来理解:

想象你有一家热门奶茶店,若只有一个收银员时,此时队伍会排得很长
而如果开了 3 个收银台,并有一个引导员把顾客平均分配到各窗口,整体效率就大幅提升 —— 这个“引导员”就是负载均衡器

负载均衡的核心作用

1. 分发请求:把用户请求(如 HTTP 请求)分给后端多个服务器。

2. 健康检查:自动检测服务器是否宕机,剔除故障节点。

3. 缓存与压缩:部分负载均衡器还能缓存静态内容,加速响应

常见负载均衡策略

策略

说明

轮询(Round Robin)

依次轮流分配请求(最简单常用)

加权轮询

性能强的服务器分配更多请求(如 A 权重 3,B 权重 1)

最少连接

把请求发给当前连接数最少的服务器

IP Hash

根据用户 IP 固定分配到某台服务器(实现会话保持)

响应时间优先

选择响应最快、负载最低的服务器

负载均衡的实现可分为 服务端负载均衡客户端负载均衡,这两种不同的流量分发策略核心区别在于:"谁来决定请求发给哪台后端服务器?"

服务端负载均衡

独立的负载均衡器(如 Nginx、云 LB) 位于客户端和后端服务之间,统一接收所有请求并分发到后端服务器

客户端负载均衡

客户端自己决定将请求发给哪一台后端服务器。客户端需事先获取服务实例列表(通常通过服务注册中心),并在本地执行负载均衡算法

接下来,我们来学习 Spring Cloud LoadBalance

Spring Cloud LoadBalancer

Spring Cloud LoadBalancer Spring Cloud 提供的一个客户端负载均衡器,用于在微服务架构中,让服务消费者(客户端)能够从多个服务实例中智能选择一个可用实例进行调用

在 Spring Cloud Netflix Ribbon 被弃用后,Spring Cloud LoadBalancer 成为了官方推荐的替代方案

要使用 Spring Cloud LoadBalancer 实现负载均衡策略非常简单,只需要给 RestTemplate 添加 @LoadBalanced 注解即可

@Configuration public class BeanConfig { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }

IP 和端口号修改为服务名称

@Slf4j @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RestTemplate restTemplate; public OrderInfo findOrderInfoById(Integer orderId) { OrderInfo orderInfo = orderMapper.selectOrderById(orderId); // 访问 URL String url = "http://product-service/product/" + orderInfo.getProductId(); ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class); orderInfo.setProductInfo(productInfo); return orderInfo; } } 

多次发起请求   127.0.0.1:8080/order/1,观察 product-service 的日志,可以看到请求被分配到这三个实例上:

Spring Cloud LoadBalancer 仅支持两种负载均衡策略:轮询策略随机策略,默认的负载均衡策略是 轮询策略RoundRobinLoadBalancer

若需要采用 随机策略,也非常简单

自定义负载均衡策略

定义随机策略对象,并通过 @Bean 将其加载到 Spring 容器中:

public class CustomLoadBalancerConfiguration { @Bean ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) { String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); return new RandomLoadBalancer(loadBalancerClientFactory .getLazyProvider(name, ServiceInstanceListSupplier.class), name); } }

使用 @LoadBalancerClient 注解配置上述随机策略:

@LoadBalancerClient(name = "product-service", configuration = CustomLoadBalancerConfiguration.class) @Configuration public class BeanConfig { @LoadBalanced @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }
name:配置的负载均衡策略对哪个服务生效

configuration:使用的负载均衡策略

多次发起请求   127.0.0.1:8080/order/1,观察 product-service 的日志:

此时请求分布接近均匀

那么 Spring Cloud LoadBalancer 具体是如何实现负载均衡的呢?接下来我们就来看 Spring Cloud LoadBalancer 的具体实现

实现原理

Spring Cloud LoadBalancer 主要是通过 LoadBalancerInterceptor 来实现的,LoadBalancerInterceptor 会对 RestTemplate 的 请求进行拦截,然后从 Eureka 根据服务 id 获取服务列表,最后根据负载均衡算法得到真实的服务地址信息,并替换服务 id

我们来看 LoadBalancerInterceptor 的具体实现:

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor { private LoadBalancerClient loadBalancer; private LoadBalancerRequestFactory requestFactory; @Override public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { // 从请求中获取 url, 如 http://product-service/product/1001 final URI originalUri = request.getURI(); // 获取路径主机名, 也就是服务id, 即 product-service String serviceName = originalUri.getHost(); // 判断 serviceName 是否为空 Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri); // 根据服务 id 进行负载均衡,并处理请求 return this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution)); } } 

继续看 execute 实现:

public class BlockingLoadBalancerClient implements LoadBalancerClient { private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory; @Override public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException { String hint = getHint(serviceId); LoadBalancerRequestAdapter<T, TimedRequestContext> lbRequest = new LoadBalancerRequestAdapter<>(request, buildRequestContext(request, hint)); Set<LoadBalancerLifecycle> supportedLifecycleProcessors = getSupportedLifecycleProcessors(serviceId); supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest)); // 根据 serviceId 和 lbRequest(负载均衡策略)选择服务 ServiceInstance serviceInstance = choose(serviceId, lbRequest); if (serviceInstance == null) { supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete( new CompletionContext<>(CompletionContext.Status.DISCARD, lbRequest, new EmptyResponse()))); throw new IllegalStateException("No instances available for " + serviceId); } return execute(serviceId, serviceInstance, lbRequest); } @Override public <T> ServiceInstance choose(String serviceId, Request<T> request) { // 获取负载均衡器 ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory.getInstance(serviceId); if (loadBalancer == null) { return null; } // 根据负载均衡算法,从列表中选择一个服务实例 Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block(); if (loadBalancerResponse == null) { return null; } return loadBalancerResponse.getServer(); } } 

我们继续看不同负载均衡策略选择实现,先来看轮询策略RoundRobinLoadBalancer)实现:

public class RoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer { @Override public Mono<Response<ServiceInstance>> choose(Request request) { ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider .getIfAvailable(NoopServiceInstanceListSupplier::new); // 通过 processInstanceResponse 进行处理 return supplier.get(request).next() .map(serviceInstances -> processInstanceResponse(supplier, serviceInstances)); } private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) { // 调用 getInstanceResponse 获取选择的实例 Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances); if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) { ((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer()); } return serviceInstanceResponse; } private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) { if (instances.isEmpty()) { if (log.isWarnEnabled()) { log.warn("No servers available for service: " + serviceId); } return new EmptyResponse(); } if (instances.size() == 1) { return new DefaultResponse(instances.get(0)); } // 原子性地增加并返回新值 同时 任何数字 & MAX_VALUE = 保留低31位,清除符号位 int pos = this.position.incrementAndGet() & Integer.MAX_VALUE; // 将无限增长的 pos 映射到有限的实例范围内 ServiceInstance instance = instances.get(pos % instances.size()); return new DefaultResponse(instance); } }

再来看随机策略RandomLoadBalancer)实现:

public class RandomLoadBalancer implements ReactorServiceInstanceLoadBalancer { @Override public Mono<Response<ServiceInstance>> choose(Request request) { ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider .getIfAvailable(NoopServiceInstanceListSupplier::new); // 通过 processInstanceResponse 进行处理 return supplier.get(request).next() .map(serviceInstances -> processInstanceResponse(supplier, serviceInstances)); } private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) { // 调用 getInstanceResponse 获取选择的实例 Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances); if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) { ((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer()); } return serviceInstanceResponse; } private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) { if (instances.isEmpty()) { if (log.isWarnEnabled()) { log.warn("No servers available for service: " + serviceId); } return new EmptyResponse(); } // ThreadLocalRandom.current(): 获取当前线程的随机数生成器 // instances.size(): 获取可用服务实例的数量 // nextInt(bound): 生成 [0, bound) 范围内的随机整数 // 生成一个介于0(包含)和instances.size()(不包含)之间的随机整数 int index = ThreadLocalRandom.current().nextInt(instances.size()); ServiceInstance instance = instances.get(index); return new DefaultResponse(instance); } }

Read more

【C++】智能指针

【C++】智能指针

前言         上文我们学到了C++11的异常,了解到了C++与C语言处理错误的区别,异常的特点在于抛出与接收。【C++11】异常-ZEEKLOG博客         本文我们来学习C++中的下一个功能:智能指针 1.智能指针的使用场景         在上文我们知道了抛异常的知识,抛异常的“抛”这个动作一般来说是当程序出现了错误,抛出错误信息为了让我们解决。这个原本是解决错误的动作,在某些时候却称为了“铸就”错误的是罪魁祸首。         比如:我们知道执行throw,这意味着在这个局部域中throw后面的语句将不再执行,跳过一段又一段程序直到找到匹配的catch时,才会从catch这个语句进行向下执行。那么一个局部域中如果在抛出异常时申请了空间,明明可以正常销毁的,但是却因为抛异常跳过了销毁空间的语句。这就导致一个及其严重的事故:内存泄漏!         在此之前,为了防止出现内存泄漏。我们通常是将抛出的异常再次捕获,执行销毁语句后,将异常重新抛出。但是这种方法并不太好用,所以为了更好的解决这个问题:智能指针诞生了。 2.RAII和智能指针的设计

By Ne0inhk
【C++】红黑树详解(2w字详解)

【C++】红黑树详解(2w字详解)

手搓AVL树 * 手搓红黑树 * github地址 * 0. 前言 * 1. 什么是红黑树 * 概念与定义 * 红黑树示例 * 2. 红黑树的性质 * 红黑树的性质解读 * 树的路径再认识 * 3. 红黑树如何确保最长路径不超过最短路径的2倍? * 4. 红黑树的实现 * 整体架构设计 * 结点颜色的枚举类 * 红黑树的结点定义 * 红黑树设计 * 红黑树的插入实现 * 1. 空树的插入 * 2. 新插入节点的父亲为黑色 * 新结点的颜色 * 3. 新插入节点的父亲为红色 * (1)叔叔存在且为红色:变色 + 继续向上处理 * (2)叔叔不存在或叔叔为黑色:旋转 + 变色 * ①LL型:右单旋 + 变色 * ②RR型:左单旋 + 变色 * ③LR型:左右双旋 + 变色 * ①RL型:右左双旋 + 变色 * 4.

By Ne0inhk
【C++】带你手搓二叉搜索树(2w字详解)

【C++】带你手搓二叉搜索树(2w字详解)

二叉搜索树 * 二叉搜索树 * github地址 * 0. 前言 * 1. 二叉搜索树的定义 * 2. 整体架构设计 * 结点设计 * 树结构设计 * 3. 相关操作实现 * 1. 构造与析构 * 构造 * 析构 * 2. 拷贝构造与赋值 * 拷贝构造 * operator=赋值 * 3. 插入 * 迭代版插入 * 递归插入 * 4. 查找 * 迭代查找: * 递归查找: * 5. 中序遍历 * 6. 删除 * 迭代删除 * 情况一 * 情况二 * 情况三 * 完整代码与逐步说明 * 递归删除 * 4. 性能分析 * 5. 完整实现代码 * 6. 结语 二叉搜索树 github地址 有梦想的电信狗 0.

By Ne0inhk
C++ 拷贝构造函数与赋值运算符:深拷贝与浅拷贝的核心辨析

C++ 拷贝构造函数与赋值运算符:深拷贝与浅拷贝的核心辨析

C++ 拷贝构造函数与赋值运算符:深拷贝与浅拷贝的核心辨析 💡 学习目标:掌握拷贝构造函数与赋值运算符的定义及调用场景,理解深拷贝与浅拷贝的本质区别,能够在实际开发中避免内存泄漏与野指针问题。 💡 学习重点:拷贝构造函数的触发条件、浅拷贝的缺陷、深拷贝的实现方法、赋值运算符的重载原则。 一、拷贝构造函数的概念与触发场景 ✅ 结论:拷贝构造函数是一种特殊的构造函数,用于通过一个已存在的对象创建一个新对象,其参数必须是本类对象的常量引用(const 类名&)。 1.1 拷贝构造函数的语法格式 class 类名 {public:// 普通构造函数 类名(参数列表);// 拷贝构造函数 类名(const 类名& other);}; ⚠️ 注意事项: 1. 拷贝构造函数的参数必须是常量引用,使用 const 防止实参被修改,使用引用避免无限递归调用拷贝构造函数。 2. 如果没有手动定义拷贝构造函数,编译器会自动生成一个默认拷贝构造函数,实现简单的成员变量值拷贝。 1.2 拷贝构造函数的触发条件

By Ne0inhk