一、负载均衡
1、RestTemplate
在研究 eureka 源码上篇中,我们在 demo-consumer 消费者服务中定义了用 @LoadBalanced 标记的 RestTemplate,然后使用 RestTemplate 通过服务名的形式来调用远程服务 demo-producer,然后请求会轮询到两个 demo-producer 实例上。
RestTemplate 是 Spring Resources 中一个访问第三方 RESTful API 接口的网络请求框架。RestTemplate 是用来消费 REST 服务的,所以 RestTemplate 的主要方法都与 REST 的 Http协议的一些方法紧密相连,例如 HEAD、GET、POST、PUT、DELETE 和 OPTIONS 等方法,这些方法在 RestTemplate 类对应的方法为 headForHeaders()、getForObject()、postForObject()、put() 和 delete() 等。
RestTemplate 本身是不具备负载均衡的能力的,如果 RestTemplate 未使用 @LoadBalanced 标记,就通过服务名的形式来调用,必然会报错。用 @LoadBalanced 标记后,调用 RestTemplate 的 REST 方法就会通过负载均衡的方式通过一定的策略路由到某个服务实例上,底层负责负载均衡的组件就是 Ribbon。后面我们再来看 @LoadBalanced 是如何让 RestTemplate 具备负载均衡的能力
2、Ribbon 与负载均衡
① 负载均衡
负载均衡是指将负载分摊到多个执行单元上,负载均衡主要可以分为集中式负载均衡与进程内负载均衡:
- 集中式负载均衡指位于因特网与执行单元之间,并负责把网络请求转发到各个执行单元上,比如 Nginx、F5。集中式负载均衡也可以称为服务端负载均衡。
- 进程内负载均衡是将负载均衡逻辑集成到客户端上,客户端维护了一份服务提供者的实例列表,实例列表一般会从注册中心比如 Eureka 中获取。有了实例列表,就可以通过负载均衡策略将请求分摊给多个服务提供者,从而达到负载均衡的目的。进程内负载均衡一般也称为客户端负载均衡。
Ribbon 是一个客户端负载均衡器,可以很好地控制 HTTP 和 TCP 客户端的负载均衡行为。Ribbon 是 Netflix 公司开源的一个负载均衡组件,已经整合到 SpringCloud 生态中,它在 Spring Cloud 生态内是一个不可缺少的组件,少了它,服务便不能横向扩展。
② Ribbon 模块
Ribbon 有很多子模块,官方文档中说明,目前 Netflix 公司主要用于生产环境的 Ribbon 子模块如下:
- ribbon-loadbalancer:可以独立使用或与其他模块一起使用的负载均衡器 API。
- ribbon-eureka:Ribbon 结合 Eureka 客户端的 API,为负载均衡器提供动态服务注册列表信息。
- ribbon-core:Ribbon 的核心API。
③ springcloud 与 ribbon 整合
与 eureka 整合到 springcloud 类似,springcloud 提供了对应的
spring-cloud-starter-netflix-eureka-client(server) 依赖包,ribbon 则整合到了 spring-cloud-starter-netflix-ribbon 中。一般也不需要单独引入 ribbon 的依赖包,spring-cloud-starter-netflix-eureka-client 中已经依赖了 spring-cloud-starter-netflix-ribbon。因此我们引入了 spring-cloud-starter-netflix-eureka-client 就可以使用 Ribbon 的功能了。
④ Ribbon 与 RestTemplate 整合使用
在 Spring Cloud 构建的微服务系统中,Ribbon 作为服务消费者的负载均衡器,有两种使用方式,一种是和 RestTemplate 相结合,另一种是和 Feign 相结合。前面已经演示了带有负载均衡的 RestTemplate 的使用,下面用一张图来看看 RestTemplate 基于 Ribbon 的远程调用。
二、RestTemplate 负载均衡
1、@LoadBalanced 注解
以 RestTemplate 为切入点,来看 Ribbon 的负载均衡核心原理。那么首先就要先看看 @LoadBalanced 注解如何让 RestTemplate 具备负载均衡的能力了。
首先看 @LoadBalanced 这个注解的定义,可以得到如下信息:
- 这个注解使用 @Qualifier 标记,其它地方就可以注入 LoadBalanced 注解的 bean 对象。
- 从注释中可以了解到,@LoadBalanced 标记的 RestTemplate 或 WebClient 将使用 LoadBalancerClient 来配置 bean 对象。
注意 @LoadBalanced 是 spring-cloud-commons 模块下 loadbalancer 包下的。
2、RestTemplate 负载均衡自动化配置
在 @LoadBalanced 同包下,有一个
LoadBalancerAutoConfiguration 自动化配置类,从注释也可以看出,这是客户端负载均衡 Ribbon 的自动化配置类。
从这个自动化配置类可以得到如下信息:
- 首先要有 RestTemplate 依赖和定义了 LoadBalancerClient 对象的前提下才会触发这个自动化配置类,这也对应了前面,RestTemplate 要用 LoadBalancerClient 来配置。
- 接着可以看到这个类注入了带有 @LoadBalanced 标识的 RestTemplate 对象,就是要对这部分对象增加负载均衡的能力。
- 从 SmartInitializingSingleton 的构造中可以看到,就是在 bean 初始化完成后,用 RestTemplateCustomizer 定制化 RestTemplate。
- 再往下可以看到,RestTemplateCustomizer 其实就是向 RestTemplate 中添加了 LoadBalancerInterceptor 这个拦截器。
- 而 LoadBalancerInterceptor 的构建又需要 LoadBalancerClient 和 LoadBalancerRequestFactory,LoadBalancerRequestFactory 则通过 LoadBalancerClient 和 LoadBalancerRequestTransformer 构造完成。
3、RestTemplate 拦截器 LoadBalancerInterceptor
LoadBalancerAutoConfiguration 自动化配置主要就是给 RestTemplate 添加了一个负载均衡拦截器 LoadBalancerInterceptor。从 setInterceptors 的参数可以看出,拦截器的类型是 ClientHttpRequestInterceptor,如果我们想定制化 RestTemplate,就可以实现这个接口来定制化,然后还可以用 @Order 标记拦截器的先后顺序。
interceptors 拦截器是在 RestTemplate 的父类 InterceptingHttpAccessor 中的, RestTemplate 的类结构如下图所示。
从 restTemplate.getForEntity("
http://demo-producer/v1/uuid", String.class) 这个GET请求进去看看,是如何使用 LoadBalancerInterceptor 的。一步步进去,可以看到最终是进入到 doExecute 这个方法了。
在 doExecute 方法中,首先根据 url、method 创建一个 ClientHttpRequest,然后利用 ClientHttpRequest 来发起请求。
InterceptingHttpAccessor 中重写了父类 HttpAccessor 的 getRequestFactory 方法,父类默认的 requestFactory 是
SimpleClientHttpRequestFactory。
重写后的 getRequestFactory 方法中,如果拦截器不为空,则基于父类默认的
SimpleClientHttpRequestFactory 和拦截器创建了 InterceptingClientHttpRequestFactory。
也就是说调用了
InterceptingClientHttpRequestFactory 的 createRequest 方法来创建 ClientHttpRequest。进去可以看到,ClientHttpRequest 的实际类型就是 InterceptingClientHttpRequest。
protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory) {
return new InterceptingClientHttpRequest(requestFactory, this.interceptors, uri, httpMethod);
}
InterceptingClientHttpRequest 的类结构如下:
RestTemplate 的 doExecute 中调用 request.execute() 其实是调用了
InterceptingClientHttpRequest 父类 AbstractClientHttpRequest 中的 execute 方法。一步步进去可以发现最终其实是调用了 InterceptingClientHttpRequest 的 executeInternal 方法。
在
InterceptingClientHttpRequest 的 executeInternal 方法中,创建了 InterceptingRequestExecution 来执行请求。在 InterceptingRequestExecution 的 execute 方法中,会先遍历执行所有拦截器,然后通过 ClientHttpRequest 发起真正的 http 请求。
进入到 LoadBalancerInterceptor 的 intercept 拦截方法内,可以看到从请求的原始地址中获取了服务名称,然后调用了 loadBalancer 的 execute 方法,也就是 LoadBalancerClient。
到这里,其实已经可以想象,loadBalancer.execute 这行代码就是根据服务名称去获取一个具体的实例,然后将原始地址替换为实例的IP地址。那这个 loadBalancer 又是什么呢?
4、负载均衡客户端 LoadBalancerClient
在配置 LoadBalancerInterceptor 时,需要两个参数,LoadBalancerClient 和
LoadBalancerRequestFactory,LoadBalancerRequestFactory前面已经知道是如何创建的了。LoadBalancerClient 又是在哪创建的呢?通过 IDEA 搜索,可以发现是在 spring-cloud-netflix-ribbon 模块下的 RibbonAutoConfiguration 中配置的,可以看到 LoadBalancerClient 的实际类型是 RibbonLoadBalancerClient。
配置类的顺序是
EurekaClientAutoConfiguration、RibbonAutoConfiguration、LoadBalancerAutoConfiguration,因为使 RestTemplate 具备负载均衡的能力需要 LoadBalancerInterceptor 拦截器,创建 LoadBalancerInterceptor 又需要 LoadBalancerClient,而 LoadBalancerClient 底层要根据服务名获取某个实例,肯定又需要一个实例库,比如从配置文件、注册中心获取。从这里就可以看出来,RibbonLoadBalancerClient 默认会从 Eureka 注册中心获取实例。
LoadBalancerClient 主要提供了三个接口:
public interface LoadBalancerClient extends ServiceInstanceChooser {
// 从 LoadBalancer 找一个 Server 来发送请求
<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
// 从传入的 ServiceInstance 取 Server 来发送请求
<T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
// 对原始 URI 重构
URI reconstructURI(ServiceInstance instance, URI original);
}
进入到 RibbonLoadBalancerClient 的 execute 方法中可以看到:
- 首先根据服务名获取服务对应的负载均衡器 ILoadBalancer。
- 然后从 ILoadBalancer 中根据一定策略选出一个实例 Server。
- 然后将 server、serviceId 等信息封装到 RibbonServer 中,也就是一个服务实例 ServiceInstance。
- 最后调用了 LoadBalancerRequest 的 apply,并传入 ServiceInstance,将地址中的服务名替换为真实的IP地址。
这个 LoadBalancerRequest 其实就是 LoadBalancerInterceptor 的 intercept 中创建的一个匿名类,在它的函数式接口内,主要是用装饰器 ServiceRequestWrapper 将 request 包了一层。
ServiceRequestWrapper 主要就是重写了 getURI 方法,在重写的 getURI 方法内,它用 loadBalancer 对 URI 进行了重构,进去可以发现,就是将原始地址中的服务名替换为 Server 的真实IP、端口地址。
@Override
public URI getURI() {
// 重构 URI
URI uri = this.loadBalancer.reconstructURI(this.instance, getRequest().getURI());
return uri;
}