Spring Cloud LoadBalancer 负载均衡实战与原理
问题背景
在微服务架构中,远程调用是常态。假设我们之前的代码是这样写的:
List<ServiceInstance> instances = discoveryClient.getInstances("product-service");
// 服务可能有多个,获取第一个
EurekaServiceInstance instance = (EurekaServiceInstance) instances.get(0);
这段逻辑很简单:根据应用名称获取服务实例列表,然后直接取第一个。但如果一个服务对应多个实例呢?流量是否可以合理地分配到多个实例上?
现象观察
为了验证这个问题,我们在本地启动多个 product-service 实例。通过 IDEA 的 Copy Configuration 功能,修改 VM options 为 -Dserver.port=9091,再启动一个实例,共启动 3 个服务。

访问 Eureka 注册中心,可以看到 product-service 下确实有三个实例:

此时访问订单接口 http://127.0.0.1:8080/order/1,观察日志发现:请求多次访问,都是同一台机器。这显然不是我们想要的结果,启动多个实例本意是为了分担负荷。
手动轮询方案
我们可以对上述代码进行简单修改,引入轮询逻辑:
private static AtomicInteger atomicInteger = new AtomicInteger(1);
private static List<ServiceInstance> instances;
@PostConstruct
public void init() {
// 根据应用名称获取服务列表
instances = discoveryClient.getInstances("product-service");
}
public OrderInfo selectOrderById(Integer orderId) {
OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
// 轮询获取实例
int index = atomicInteger.getAndIncrement() % instances.size();
ServiceInstance instance = 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;
}
修改后再次启动,请求会被均衡地分配在不同的实例上,这就是最基础的负载均衡实现。
什么是负载均衡
负载均衡(Load Balance,简称 LB)是高并发、高可用系统必不可少的关键组件。当服务流量增大时,通常会采用增加机器的方式进行扩容,负载均衡就是用来在多个机器或者其他资源中,按照一定的规则合理分配负载。
打个比方:一个团队最开始只有一个人,后来随着工作量的增加,公司又招聘了几个人。负载均衡就是如何把工作量均衡地分配到这几个人身上,以提高整个团队的效率。
负载均衡的实现方式
上面的例子中,我们只是简单的对实例进行了轮询,但真实的业务场景会更加复杂。比如根据机器的配置进行负载分配,配置高的分配的流量高,配置低的分配流量低等。类似企业员工:能力强的员工可以多承担一些工作。
服务多机部署时,开发人员都需要考虑负载均衡的实现,所以也出现了一些负载均衡器,来帮助我们实现负载均衡。主要分为两类:
服务端负载均衡
在服务端进行负载均衡的算法分配。比较有名的服务端负载均衡器是 Nginx。请求先到达 Nginx 负载均衡器,然后通过负载均衡算法,在多个服务器之间选择一个进行访问。
客户端负载均衡
在客户端进行负载均衡的算法分配。把负载均衡的功能以库的方式集成到客户端,而不再是由一台指定的负载均衡设备集中提供。
比如 Spring Cloud 的 Ribbon,请求发送到客户端,客户端从注册中心(比如 Eureka)获取服务列表,在发送请求前通过负载均衡算法选择一个服务器,然后进行访问。Ribbon 是 Spring Cloud 早期的默认实现,由于不再维护,所以最新版本的 Spring Cloud 负载均衡集成的是 Spring Cloud LoadBalancer(由 Spring Cloud 官方维护)。
客户端负载均衡和服务端负载均衡最大的区别在于服务清单所存储的位置。
Spring Cloud LoadBalancer 实战
快速上手
1. 配置 RestTemplate
给 RestTemplate 这个 Bean 添加 @LoadBalanced 注解就可以启用负载均衡功能:
@Configuration
public class BeanConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
2. 修改调用地址
将 IP 端口号改为服务名称:
public OrderInfo selectOrderById(Integer orderId) {
OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
// 使用服务名代替具体 IP 和端口
String url = "http://product-service/product/" + orderInfo.getProductId();
ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
orderInfo.setProductInfo(productInfo);
return orderInfo;
}
3. 测试效果
启动多个 product-service 实例后,观察日志,你会发现请求被自动分配到了不同的实例上,无需手动维护轮询计数器。
负载均衡策略
负载均衡策略是一种思想,无论是哪种负载均衡器,它们的负载均衡策略都是相似的。Spring Cloud LoadBalancer 仅支持两种负载均衡策略:轮询策略 和 随机策略。
- 轮询 (Round Robin):服务器轮流处理用户的请求。这是一种实现最简单,也最常用的策略。生活中也有类似的场景,比如学校轮流值日,或者轮流打扫卫生。
- 随机选择 (Random):随机选择一个后端服务器来处理新的请求。
自定义负载均衡策略
Spring Cloud LoadBalancer 默认负载均衡策略是轮询策略(RoundRobinLoadBalancer)。如果服务的消费者想采用随机的负载均衡策略,也非常简单。
第一步:定义随机算法对象
通过 @Bean 将其加载到 Spring 容器中。此处使用 Spring Cloud LoadBalancer 提供的 RandomLoadBalancer:
public class LoadBalancerConfig {
@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
System.out.println("==============" + name);
return new RandomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
name
);
}
}
注意:该类需要满足以下两点:
- 不用加
@Configuration注解。- 必须在组件扫描范围内。
第二步:绑定策略
使用 @LoadBalancerClient 或 @LoadBalancerClients 注解在 RestTemplate 配置类上方。可以对不同的服务提供方配置不同的客户端负载均衡算法策略。由于项目中只有一个服务提供者,所以直接使用 @LoadBalancerClient:
@LoadBalancerClient(name = "product-service", configuration = LoadBalancerConfig.class)
@Configuration
public class BeanConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
注解说明:
name:该负载均衡策略对哪个服务生效(服务提供方)。configuration:该负载均衡策略用哪个负载均衡策略实现。
LoadBalancer 原理分析
LoadBalancer 的实现,核心在于 LoadBalancerInterceptor。这个类会对 RestTemplate 的请求进行拦截,然后从 Eureka 根据服务 ID 获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务 ID。
我们来看看源码实现的关键部分:
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
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 方法,拦截了用户的 HTTP 请求,然后做了几件事:
request.getURI():从请求中获取 URI。originalUri.getHost():从 URI 中获取路径的主机名,也就是服务 ID(例如product-service)。loadBalancer.execute:根据服务 ID,进行负载均衡,并处理请求。
后续的核心逻辑在 BlockingLoadBalancerClient 中:
public class BlockingLoadBalancerClient implements LoadBalancerClient {
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
String hint = this.getHint(serviceId);
LoadBalancerRequestAdapter<T, TimedRequestContext> lbRequest = new LoadBalancerRequestAdapter(request, this.buildRequestContext(request, hint));
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = this.getSupportedLifecycleProcessors(serviceId);
supportedLifecycleProcessors.forEach((lifecycle) -> {
lifecycle.onStart(lbRequest);
});
// 根据 serviceId,和负载均衡策略,选择处理的服务
ServiceInstance serviceInstance = this.choose(serviceId, lbRequest);
if (serviceInstance == null) {
supportedLifecycleProcessors.forEach((lifecycle) -> {
lifecycle.onComplete(new CompletionContext(Status.DISCARD, lbRequest, new EmptyResponse()));
});
throw new IllegalStateException("No instances available for " + serviceId);
} else {
return this.execute(serviceId, serviceInstance, lbRequest);
}
}
/**
* 根据 serviceId,和负载均衡策略,选择处理的服务
*/
public <T> ServiceInstance choose(String serviceId, Request<T> request) {
// 获取负载均衡器
ReactiveLoadBalancer<ServiceInstance> loadBalancer = this.loadBalancerClientFactory.getInstance(serviceId);
if (loadBalancer == null) {
return null;
} else {
// 根据负载均衡算法,在列表中选择⼀个服务实例
Response<ServiceInstance> loadBalancerResponse = (Response) Mono.from(loadBalancer.choose(request)).block();
return loadBalancerResponse == null ? null : (ServiceInstance) loadBalancerResponse.getServer();
}
}
}
整个过程就是:拦截请求 -> 解析服务名 -> 获取实例列表 -> 执行策略选择 -> 替换地址 -> 发起实际调用。

