专栏:Spring Cloud
个人主页:手握风云
目录
一、负载均衡
1.1. 问题
1.2. 定义
1.3. 分类
二、Spring Cloud LoadBalancer
2.1. 快速上手
2.2. 负载均衡策略
2.3. 底层原理
三、多机部署
3.1. 前置准备
3.2. Maven 项目打包
3.3. 服务器 Jar 包上传与后台启动
一、负载均衡
1.1. 问题
在前面的远程调用基础代码中,程序会通过 DiscoveryClient 根据服务名称拉取完整的服务实例列表,但代码逻辑固定选取列表下标为 0 的第一个实例发起请求。这种写法在服务仅部署单实例时不会出现问题,可一旦同一服务启动多份实例,流量会全部集中到首个实例,多节点扩容的价值完全无法发挥。
右键服务启动配置选择 Copy Configuration 复制配置,再通过 Modify options 菜单栏添加 VM 启动参数 -Dserver.port=端口号,以此修改不同实例的占用端口。本次演示共启动 3 个 product-service 服务,端口分别为 9090、9091、9092,全部注册至 Eureka 注册中心。
package com.yang.order.service; import com.yang.order.mapper.OrderMapper; import com.yang.order.model.OrderInfo; import com.yang.order.model.ProductInfo; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @Slf4j @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; private AtomicInteger count = new AtomicInteger(1); private List<ServiceInstance> instances; @PostConstruct public void init() { //从Eureka中获取服务列表 instances = discoveryClient.getInstances("product-service"); } public OrderInfo selectOrderById(Integer orderId) { OrderInfo orderInfo = orderMapper.selectOrderById(orderId); // 通过计数器 % 实例 = 下标索引 int index = count.getAndIncrement() % instances.size(); String uri = instances.get(index).getUri().toString(); String url = uri + "/product/" + orderInfo.getProductId(); log.info("远程调用url:{}", url); ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class); orderInfo.setProductInfo(productInfo); return orderInfo; } }借助 AtomicInteger 实现原子自增计数,通过计数值对实例列表长度取模,循环选中不同服务实例,改造代码后重新测试,请求会均匀分配至三台实例,直观展现出负载均衡的基础效果。
1.2. 定义
负载均衡英文缩写为 LB,是构建高并发、高可用分布式系统不可或缺的核心组件。随着业务流量持续上涨,开发者通常会通过新增服务节点完成水平扩容,而负载均衡的核心作用,就是按照预设规则,将客户端流量合理分摊到多台服务机器,避免单节点压力过载,整体提升集群的处理效率。
团队初期仅一名员工,工作量提升后新增多名员工,负载均衡就相当于分配工作的规则,均衡分摊任务,充分发挥团队整体处理能力,和多服务实例分流流量的设计思路完全一致。
1.3. 分类
真实业务场景下负载均衡的分配规则十分丰富,除基础轮询外,还可根据服务器硬件性能分配流量,硬件配置更高的节点承载更多请求。为统一标准化实现,行业内衍生出两类成熟的负载均衡架构,分别是服务端负载均衡与客户端负载均衡,二者最核心的区分标准是服务实例清单的存储位置。
第一类是服务端负载均衡,典型代表工具为 Nginx。整体流程为用户请求先统一发送到独立的负载均衡中间设备,由均衡器存储全部后端服务清单,通过内置算法挑选节点后转发请求,所有流量都需要经过这一台集中式设备中转。
第二类是客户端负载均衡,功能以 SDK 类库的形式集成在调用方服务内部,无需独立中间设备。Spring Cloud 早期采用 Ribbon 组件实现该能力,但该组件已停止维护,2020.0.1 及以上新版本统一使用官方自研的 Spring Cloud LoadBalancer。执行流程为客户端先向 Eureka 注册中心拉取全部可用服务实例列表,在本地运行负载均衡算法筛选目标节点,之后直接与选中的服务实例建立连接发起调用。
二、Spring Cloud LoadBalancer
2.1. 快速上手
Spring Cloud 在 2020.0.1 版本之后正式移除了原有的 Ribbon 组件,改用 Spring Cloud LoadBalancer 作为官方默认的客户端负载均衡实现方案,也是当前新项目统一采用的标准组件。想要快速启用该组件实现负载均衡,仅需完成两步核心配置。
第一步是改造 RestTemplate 的 Bean 配置,在创建 RestTemplate 的方法上添加 @LoadBalanced 注解,该注解会开启负载均衡拦截能力,为后续服务名转发提供基础支撑。第二步需要修改远程调用的请求地址,摒弃硬编码的 IP 与端口,直接使用注册中心内注册的服务名称拼接 URL,例如把 http://127.0.0.1:9090/product/{id} 替换为 http://product-service/product/{id}。
package com.yang.order.config; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer; import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; 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); } }package com.yang.order.config; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @LoadBalancerClient(name = "product-service", configuration = CustomLoadBalancerConfiguration.class) @Configuration public class BeanConfig { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }Spring Cloud LoadBalancer
package com.yang.order.service; import com.yang.order.mapper.OrderMapper; import com.yang.order.model.OrderInfo; import com.yang.order.model.ProductInfo; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** * @author gao * @date 2026/7/22 11:34 */ @Slf4j @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; private AtomicInteger count = new AtomicInteger(1); private List<ServiceInstance> instances; @PostConstruct public void init() { //从Eureka中获取服务列表 instances = discoveryClient.getInstances("product-service"); } public OrderInfo selectOrderById(Integer orderId) { OrderInfo orderInfo = orderMapper.selectOrderById(orderId); String url = "http://product-service/product/" + orderInfo.getProductId(); log.info("远程调用url:{}", url); ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class); orderInfo.setProductInfo(productInfo); return orderInfo; } }持续重复调用订单接口 http://127.0.0.1:8080/order/1 进行功能验证,查看三个 product-service 实例的运行日志能够清晰发现,请求会自动分发到三台不同实例,负载均衡能力正式生效。
2.2. 负载均衡策略
负载均衡策略是一套通用的流量分配逻辑,不管是服务端均衡器还是客户端均衡组件,底层设计思路都高度相似。Spring Cloud LoadBalancer 内置仅提供两种原生负载均衡策略,分别是轮询策略与随机策略。
轮询策略 RoundRobin 是组件默认使用的分配方案,逻辑为请求按照顺序轮流分配给集群内每一台可用实例,实现简单、适用绝大多数常规业务场景。随机选择 Random 策略则是每次发起请求时,从全部可用实例中随机挑选一台处理请求,分配逻辑无固定顺序。
如果业务场景需要切换为随机策略,支持自定义负载均衡算法实现。首先需要新建负载均衡配置类 LoadBalancerConfig,在类中注入 RandomLoadBalancer 类型的 Bean,同时有两个硬性要求:配置类不能标注 @Configuration 注解,且该类需要处于 Spring 组件扫描范围内。
完成策略 Bean 编写后,还需要借助 @LoadBalancerClient 注解绑定策略与目标服务。该注解添加在 RestTemplate 配置类上方,其中 name 属性指定需要生效负载均衡策略的服务名称,configuration 属性指向自定义的策略配置类。项目中仅存在 product-service 一个服务提供者,因此直接使用单服务绑定注解@LoadBalancerClient即可完成配置。
2.3. 底层原理
Spring Cloud LoadBalancer 的核心拦截载体是 LoadBalancerInterceptor,该拦截器会拦截所有通过 RestTemplate 发起的 HTTP 远程调用,自动完成服务名解析、实例筛选、地址替换全流程操作。
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor { private LoadBalancerClient loadBalancer; private LoadBalancerRequestFactory requestFactory; public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) { this.loadBalancer = loadBalancer; this.requestFactory = requestFactory; } public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) { this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer)); } public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { URI originalUri = request.getURI(); String serviceName = originalUri.getHost(); Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri); return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution)); } }当请求进入 intercept 拦截方法后,第一步会读取请求原始 URI,提取 URI 中的 Host 作为服务 ID,也就是我们拼接 URL 时填写的服务名称 product-service;第二步调用 LoadBalancerClient 的 execute 执行方法,传入解析得到的服务 ID,进入实例选择逻辑。
在 BlockingLoadBalancerClient 的 execute 方法内部,会先调用 choose 方法执行负载均衡筛选。程序会根据服务 ID 获取对应的负载均衡器,再调用均衡器内置的 choose 方法,基于预设的轮询或随机策略,从 Eureka 拉取的服务实例列表中选出一台可用 ServiceInstance。
如果筛选后没有找到任何可用实例,程序会直接抛出无服务实例异常;若成功获取有效实例,则将请求 URL 中的服务名替换为实例真实 IP 与端口,最终发起远程调用,完成一整套客户端负载均衡流程。
三、多机部署
3.1. 前置准备
- 服务器安装 MySQL,导入业务初始化 SQL;
- 修改项目配置文件,替换数据库连接账号、密码适配服务器 MySQL。
3.2. Maven 项目打包
分别对 3 个微服务打包:eureka-server、order-service、product-service,标准 SpringBoot Maven 打包流程。
3.3. 服务器 Jar 包上传与后台启动
- Eureka、Order 单实例启动;
- Product 多实例通过 --server.port=端口 区分端口,分别输出独立日志文件