在微服务架构盛行的当下,我们会借助 Spring Cloud 等主流的微服务开发框架来实现各个微服务。而在服务与服务的调用过程中,负载均衡是一项基本技术手段,用来确保系统高可用的同时也能实现对请求的有效分发,从而提升系统性能。
今天我们讨论负载均衡的切入点并不是它的基本概念和原理,而是专注于 Spring Cloud 框架对负载均衡实现过程所提供的开发友好性。熟悉 Spring Cloud 的人,应该知道,我们想要在服务调用过程中嵌入负载均衡机制,要做的事情就只有一件,就是在 RestTemplate 模板工具类上添加一个@LoadBalanced 注解。
less
@LoadBalanced
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
这里的 RestTemplate 是 Spring 自带的一个 HTTP 请求工具类,本身并具备负载均衡能力。你可能会觉得奇怪,为什么在这个工具类上添加了@LoadBalanced 注解就能自动嵌入负载均衡机制呢?这个@LoadBalanced 注解背后的工作原理又是怎么样的呢?这就是今天我们要分析的内容,我们接着往下看。
Spring Cloud 中的@LoadBalanced
让我们打开 Spring Cloud 源码,来到 spring-cloud-commons 这个代码工程,可以发现虽然这个工程的名称是 common,但内置了大量以 client 命名的代码包。这些代码包中就包含了与服务发现、负载均衡相关的所有基础类定义,我们要介绍的@LoadBalanced 注解也位于这些代码包中。

spring-cloud-commons 代码工程图
事实上,在 Spring Cloud 中存在一个自动配置类 LoadBalancerAutoConfiguration 类。而在这个类中,维护着一个被@LoadBalanced 修饰的 RestTemplate 对象列表。
less
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
针对这些被@LoadBalanced 注解修饰的 RestTemplate,在 LoadBalancerAutoConfiguration 初始化的过程中,Spring 容器会调用 RestTemplateCustomizer 的 customize 方法进行定制化,这个定制化的过程就是对目标 RestTemplate 增加拦截器 LoadBalancerInterceptor。
less
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
//为 RestTemplate 添加拦截器
restTemplate.setInterceptors(list);
};
}
这里就用到了 RestTemplate 的拦截器扩展机制。有时候我们需要对 HTTP 请求做一些通用的拦截设置,就会使用到拦截器。这些拦截器需要实现 ClientHttpRequestInterceptor 接口,而这里的 LoadBalancerInterceptor 就是用于对请求进行拦截。
我们在它的构造函数中发现传入了一个 LoadBalancerClient,而在它的拦截方法中,本质上就是使用这个 LoadBalanceClient 来执行真正的负载均衡。LoadBalancerInterceptor 类代码如下所示:
java
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
...
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
}
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
//通过 LoadBalancerClient 执行负载均衡
returnthis.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
}
可以看到,这里的 intercept 拦截方法中直接调用了 LoadBalancerClient 接口的 execute 方法完成对请求的负载均衡执行。而这个方法的输入参数有两个,一个是代表服务名称的 serviceName,另一个则是代表负载均衡请求对象的 LoadBalancerRequest。具体的 LoadBalancerRequest 是如下所示的一个 ServiceRequestWrapper 包装类:
kotlin
public class ServiceRequestWrapper extends HttpRequestWrapper {
privatefinal ServiceInstance instance;
privatefinal LoadBalancerClient loadBalancer;
public ServiceRequestWrapper(HttpRequest request, ServiceInstance instance, LoadBalancerClient loadBalancer) {
super(request);
this.instance = instance;
this.loadBalancer = loadBalancer;
}
@Override
public URI getURI() {
URI uri = this.loadBalancer.reconstructURI(this.instance, getRequest().getURI());
return uri;
}
}
这段代码中同样出现了 LoadBalanceClient,并用它来完成了请求地址 URI 的构建。显然,LoadBalanceClient 是我们分析负载均衡机制的核心入口。接下来,我们就对这个接口及其实现类进行详细的展开。
LoadBalancerClient 接口与实现类
LoadBalancerClient 是一个非常重要的接口,定义如下:
java
public interface LoadBalancerClient extends ServiceInstanceChooser {
//执行负载均衡调用
<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
//执行负载均衡调用
<T> T execute(String serviceId, ServiceInstance serviceInstance,
LoadBalancerRequest<T> request) throws IOException;
//构建负载均衡调用 URI
URI reconstructURI(ServiceInstance instance, URI original);
}
这里有两个 execute 重载方法,用于根据负载均衡器所确定的服务实例来执行服务调用。而 reconstructURI 方法则用于构建服务 URI,基于负载均衡所选择的 ServiceInstance 信息,并利用服务实例的 host、port 以及端点路径,我们就可以构造一个真正可供访问的服务地址。
同时,我们发现 LoadBalancerClient 有一个父接口------ServiceInstanceChooser,这个接口的定义如下:
arduino
public interface ServiceInstanceChooser {
//根据 serviceId 选择目标服务实例
ServiceInstance choose(String serviceId);
//根据 serviceId 和请求选择目标服务实例
<T> ServiceInstance choose(String serviceId, Request<T> request);
}
显然,从负载均衡角度讲,我们应该重点关注实际上是这两个 choose 方法的实现,因为它们完成了对目标服务实例的具体选择过程,而这个选择过程集成了各种负载均衡算法。
在 Spring Cloud 中,针对 LoadBalancerClient 接口有一组实现类,包括我们要介绍的这个 Spring Cloud Netflix 中的 RibbonLoadBalancerClient 类。这个类基于 Netflix Ribbon 组件实现了负载均衡机制,是 Spring Cloud 中最早、最经典的一种负载均衡实现方式。
这里我们有必要梳理一下 Netflix Ribbon 和 Spring Cloud 之间的关系。Netflix Ribbon 是来自 Netflix 的一个外部组件,它提供的只是一个辅助工具。这个辅助工具的目的是让你去集成它,而不是说它自己完成所有的工作。Spring Cloud Netflix Ribbon 就是 Spring Cloud 专门针对 Netflix Ribbon 提供的一个独立的集成实现。
对于 Netflix Ribbon 而言,Spring Cloud Netflix Ribbon 相当于它的客户端;对于 Spring Cloud Netflix Ribbon 来说,我们的应用服务相当于它的客户端。Netflix Ribbon、Spring Cloud Netflix Ribbon、应用服务这三者之间的关系以及核心入口是这样的:

Spring Cloud 负载均衡三大组件之间的关系图
在 RibbonLoadBalancerClient 中,我们可以看到它的 choose 方法是调用了一个 getServer 方法来获取服务器信息,而这个 getServer 方法则是通过 ILoadBalancer 接口完成了对目标服务器的选择,对应代码如下所示:
typescript
public ServiceInstance choose(String serviceId, Object hint) {
Server server = getServer(getLoadBalancer(serviceId), hint);
...
}
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
...
return loadBalancer.chooseServer(hint != null ? hint : "default");
}
这个 ILoadBalancer 就来自于 Netflix Ribbon,这个接口位于 com.netflix.loadbalancer 包下,定义是这样的:
arduino
public interface ILoadBalancer {
//添加后端服务
public void addServers(List<Server> newServers);
//选择一个后端服务
public Server chooseServer(Object key);
//标记一个服务不可用
public void markServerDown(Server server);
//获取当前可用的服务列表
public List<Server> getReachableServers();
//获取所有后端服务列表
public List<Server> getAllServers();
}
针对负载均衡,我们重点应该关注的是 ILoadBalancer 接口中 chooseServer 方法。从方法命名上,我们不难理解这个方法完成了对某一个服务端实例的选择过程,从而包含了具体的负载均衡实现过程。
ini
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
returnnull;
} else {
try {
return rule.choose(key);
} catch (Exception e) {
returnnull;
}
}
}
可以看到,这里使用了一个 IRule 接口集成了具体负载均衡策略的实现。IRule 接口是对负载均衡策略的一种抽象,可以通过实现这个接口来提供各种负载均衡算法,代码示例是这样的:
csharp
public interface IRule{
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}
接下来就让我们看看 Ribbon 中的 IRule 接口为我们提供了具体哪些负载均衡算法。回到 Netflix Ribbon,IRule 接口的类层结构如下图所示:

IRule 接口的类层结构图
可以看到,Netflix Ribbon 中的负载均衡实现策略是非常丰富的,既提供了 RandomRule、RoundRobinRule 等无状态的静态策略,又实现了 AvailabilityFilteringRule、WeightedResponseTimeRule 等多种基于服务器运行状况进行实时路由的动态策略。
在上图中,我们还看到了 RetryRule 这种重试策略,这种策略会对选定的负载均衡策略执行重试机制。严格意义上讲,重试是一种服务容错而不是负载均衡机制,但 Ribbon 也内置了这方面的功能。
事实上,我们也可以基于 IRule 接口实现任何定制化的负载均衡算法,然后通过配置的方式加载到 Spring Cloud 中,示例代码如下所示:
less
@Configuration
public class LoadBalanceConfig{
@Autowired
IClientConfig config;
@Bean
@ConditionalOnMissingBean
public IRule customRule(IClientConfig config) {
return new RandomRule();
}
}
这个配置类的作用是使用 RandomRule 替换 Ribbon 中的默认负载均衡策略------RoundRobin。
总结
好了,让我们来总结回顾一下今天的内容吧。对于一款客户端负载均衡工具而言,要做的事情无非是从服务列表中选择一个服务进行调用。为了实现这个过程,我们首先需要集成各种负载均衡算法,另一方面我们也需要提供入口供客户端请求进行使用。
Spring Cloud 就为我们提供了一种非常友好的实现方式,开发人员只需要通过一个简单的@LoadBalanced 注解就能自动在调用过程中集成负载均衡机制。