前言
Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。
这篇文章我们来看下如何使用Ribbon来实现客户端负载均衡,以及Ribbon实现负载均衡的原理。
正文
客户端负载均衡
负载均衡在系统架构中非常重要,是系统实现高可用、网络压力缓解、处理能力扩容的重要手段之一。一般情况下,我们所说的负载均衡大都指的服务端的负载均衡,通常分为硬件负载均衡和软件负载均衡。
硬件负载均衡,主要通过在服务器节点之间安装专门用于负载均衡的设备来实现,如F5。
软件负载均衡,主要通过在服务器上安装一些具有负载均衡的模块或者软件来实现,如Nginx。
负载均衡设备或者软件都会维护一个下挂可用的服务端清单,通过心跳检测来剔除故障的服务节点以保证清单中都是可以正常访问的服务节点。客户端发送请求到负载均衡设备,设备按照某种算法(轮询、权重等)从清单里取出一台服务地址,进行转发。
客户端负载均衡和上面说的服务端负载均衡最大的不同点在于上面提到的服务清单所存储的位置。在客户端负载均衡中,所有客户端都需要维护自己要访问的服务器清单,这些服务端清单来自注册中心。当然,客户端负载均衡也要通过心跳检测服务的健康性,这个步骤需要注册中心配合完成。
RestTemplate
在之前Eureka的例子中,我们引入了Ribbon实现负载均衡功能,同时知道了通过给RestTemplate
对象配置@LoadBalanced
注解便可以开启客户端负载均衡。
在了解Ribbon之前,我们先聊聊RestTemplate
。
我们可以看到RestTemplate
是属于spring web模块的一个类,顾名思义,它是spring用来发送REST请求的封装模板。
对于GET请求,可以看到主要有两种类型的函数方法。
第一种,getForEntity
函数。返回ResponseEntity
对象。它有三个方法:
1 | //1. |
对于第一个方法,其uriVariables里为GET请求的参数,通过url占位符的方式使用,如下:
1 | ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://SAKURA-SERVICE/hello?name={1}",String.class,"aaa"); |
其中访问SAKURA-SERVICE服务的hello接口时,”aaa”会替换掉{1}。
如果返回对象是个User,那么如下实现:
1 | ResponseEntity<User> responseEntity = restTemplate.getForEntity("http://SAKURA-SERVICE/hello?name={1}",User.class,"aaa"); |
第2,3个方法,代码如下:
1 | //2.我们使用name作为占位符,则map里需要put一个key为name的参数 |
第二种,getForObject
函数。该方法是对getForEntity
的进一步封装。
我们使用时十分简单,如下:
1 | String str = restTemplate.getForObject(uri,String.class); |
它也有3个重载方法:
1 | public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {} |
对于POST请求,与GET类似,它也有postForEntity
和postForObject
函数。
它们的方法如下:
1 | public <T> T postForObject(String url, Object request, Class<T> responseType, |
它们的使用与GET类似,我们就不在过多介绍。
1 | User user = new User("aaaa",18); |
另外POST里还有一种postForLocation
函数,用来提交资源并返回新资源URI,它也有三种重载方法。
1 | User user = new User("aaaa",18); |
1 | public URI postForLocation(String url, Object request, Object... uriVariables) |
RestTemplate
里的其他方法,如PUT、DELETE等我们不再介绍,有兴趣的可以看看源码。
Ribbon源码
分析完RestTemplate
后,我们会想,RestTemplate
本是Spring本是的东西,如何通过Ribbon实现负载均衡的呢?
因为我们之前的例子讲到当RestTemplate
作用上@LoadBalanced
注解后,便可以实现负载均衡了,因此我们从@LoadBalanced
注解开始看起吧。
通过@LoadBalanced
注释可以看到,该注解用来给RestTemplate
做标记,以使用负载均衡的客户端LoadBalancerClient
来配置它。
我们搜索LoadBalancerClient
可以发现这是Spring Cloud定义的一个接口。如下:
通过该接口,可以大致了解负载均衡客户端应具备的几种能力。
choose
方法:根据传入的服务名serviceId,从负载均衡器中挑选一个对应服务的实例。1
ServiceInstance choose(String serviceId)
execute
方法:使用从负载均衡器中挑选出来的服务实例来执行请求内容。1
2
3<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
<T> T execute(String serviceId, ServiceInstance serviceInstance,LoadBalancerRequest<T> request) throws IOException;reconstructURI
方法:为系统构建一个合适的host:port形式的URI。1
URI reconstructURI(ServiceInstance instance, URI original);
我们再来看一下LoadBalancerAutoConfiguration
这个类,这个类是实现客户端负载均衡的自动化配置类。
根据上图,我们可以知道,Ribbon实现负载均衡自动化配置需要满足下面两个条件:
@ConditionalOnClass(RestTemplate.class)
:当前工程环境中需要存在RestTemplate
类。@ConditionalOnBean(LoadBalancerClient.class)
:需要存在LoadBalancerClient
接口的实现Bean。
该自动化配置类主要完成以下几个功能:
- 创建一个
LoadBalancerInterceptor
的 Bean,用于实现对客户端发起请求时进行拦截,以实现客户端负载均衡。 - 创建一个
RetryLoadBalancerInterceptor
的Bean,用于实现客户端负载均衡的重试机制。 - 创建一个
RestTemplateCustomizer
的 Bean,用于给RestTemplate
增加LoadBalancerInterceptor
和RetryLoadBalancerInterceptor
拦截器。 - 维护一个被
@LoadBalanced
修饰的RestTemplate
对象列表,并在这里进行初始化,通过调用RestTemplateCustomizer
实例来给需要客户端负载均衡的RestTemplate
增加LoadBalancerInterceptor
和RetryLoadBalancerInterceptor
拦截器。
LoadBalancerInterceptor
相关代码如下:
通过源码以及之前的自动化配置类,我们可以看到在拦截器中注入了LoadBalancerClient
的实现。当一个被@LoadBalanced
注解修饰的RestTemplate
对象向外发起HTTP请求时,会被LoadBalancerInterceptor
类的intercept
所拦截。由于我们在使用RestTemplate
时采用了服务名作为host,所以直接从HttpRequest
的URI对象中通过getHost()
就可以拿到服务名,然后调用execute
函数去根据服务名来选择实例并发起实际的请求。
我们上面讲到了LoadBalancerClient
,它只是一个接口,我们现在来看下它的实现类。我们很容易就可以找到它的实现类RibbonLoadBalancerClient
。
这个类我们主要看下它的execute
方法,如下:
1 | public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException { |
可以看到,在execute
函数的实现中,第一步就是通过getServer
根据传入的服务名serviceId
去获取具体的服务实例。
同时可以看到getServer
函数的实现,调用的是ILoadBalancer
接口中定义的chooseServer
函数。
对于ILoadBalancer
接口,该接口定义了一个客户端负载均衡需要的一系列抽象操作。
1 | public interface ILoadBalancer { |
- addServers:向负载均衡器中维护的实例列表增加服务实例。
- chooseServer:通过某种策略,从负载均衡器中挑选出一个具体的服务实例。
- markServerDown:用来通知和标识负载均衡器中某个具体实例已经停止服务,不然负载均衡器在下一次获取服务实例清单前会认为服务实例正常,实际访问时出现问题。
- getReachableServers:获取当前正常服务的实例列表。
- getAllServers:获取所有已知服务实例列表,包括正常服务和停止服务的实例。
该接口的实现就是对负载均衡策略的一些扩展,这部分我们下节在详细讨论。
在RibbonClientConfiguration
配置类中,我们可以看到如下代码。
1 |
|
可以看到Ribbon默认使用ZoneAwareLoadBalancer
来实现负载均衡器。
我们在回到RibbonLoadBalancerClient
的代码逻辑,当通过ZoneAwareLoadBalancer
的chooseServer
函数获取了负载均衡策略分配到的服务实例Server后,将其内容包装成RibbonServer
对象,然后使用该对象回调LoadBalancerInterceptor
请求拦截器中LoadBalancerRequest
的apply(ServiceInstance instance)
函数,向一个具体服务实例发起请求。
在apply(ServiceInstance instance)
函数中传入的ServiceInstance
接口对象是对服务实例的抽象定义。其内容如下:
1 | public interface ServiceInstance { |
上面的RibbonServer
对象就是该接口对的一个实现。
1 | public static class RibbonServer implements ServiceInstance { |
总结
分析到这里,我们大致理清Spring Cloud Ribbon中实现负载均衡的基本脉络,了解了它的一些源码。后面我们会再来看下Ribbon的负载均衡器及负载均衡策略的一些东西。