上一篇文章Eureka(五)——高可用中,我们介绍了如何构建高可用的服务注册中心和服务提供。
本文我们来看看如何去消费服务提供者的接口。
使用LoadBalancerClient
在Spring Cloud Commons中提供了大量的与服务治理相关的抽象接口,包括DiscoveryClient、这里我们即将介绍的LoadBalancerClient等。Spring Cloud做这一层抽象,很好的解耦了服务治理体系,使得我们可以轻易的替换不同的服务治理设施。
从LoadBanacerClient接口的命名中,我们就知道这是一个负载均衡客户端的抽象定义,下面我们就看看如何使用Spring Cloud提供的负载均衡器客户端接口来实现服务的消费。
首先创建一个服务消费者工程,命名为:eureka-consumer。并在pom.xml中引入依赖
1 | <dependencies> |
配置application.yml,指定eureka注册中心的地址:
1 | spring: |
创建应用主类。初始化RestTemplate,用来真正发起REST请求。
1 |
|
创建一个接口用来消费eureka-client-multi提供的接口:
1 |
|
我们注入了LoadBalancerClient和RestTemplate,并在/consumer接口的实现中,先通过loadBalancerClient的choose函数来负载均衡地选出一个eureka-client的服务实例,这个服务实例的基本信息存储在ServiceInstance中,然后通过这些对象中的信息拼接出访问/dc接口的详细地址,最后再利用RestTemplate对象实现对服务提供者接口的调用。
启动eureka-consumer,这时Eureka Server的dashboard如下所示,我们看到eureka-consumer也被注册到Server中。

访问eureka-consumer的/consumer接口:http://127.0.0.1:8763/consumer,返回eureka-client-multi服务的数据:

查看eureka-consumer输出的日志我们会发现,loadBalancerClient在提供服务的三个地址之间切换:
1 | http://wangqideimac.lan:8741/dc |
LoadBalancerClient的原理
上文我们看到eureka-consumer能够成功以负载均衡的方式访问eureka-client-multi服务,下面我们来看看LoadBalancerClient的原理。
RibbonLoadBalancerClient
首先我们来看看这里LoadBalancerClient的实现类是什么,通过加断点的方式发现它的实现类是RibbonLoadBalancerClient。为什么?我们明明没有引入ribbon相关的依赖。
仔细看spring-cloud-starter-netflix-eureka-client,发现它引入了ribbon的依赖:

进入spring-cloud-starter-netflix-ribbon包,其中spring.factories文件中定义了EnableAutoConfiguration:
1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
RibbonAutoConfiguration是ribbon的配置类,其中定义了LoadBalancerClient的实例为RibbonLoadBalancerClient:
1 |
|
下图是RibbonLoadBalancerClient类的继承关系:

RibbonLoadBalancerClient实现了两个接口LoadBalancerClient和ServiceInstanceChooser。
LoadBalancerClient接口有三个方法,其中execute()为执行请求,reconstructURI()用来重构url:
1 | public interface LoadBalancerClient extends ServiceInstanceChooser { |
ServiceInstanceChooser接口,主要有一个方法,用来根据serviceId来获取ServiceInstance,代码如下:
1 | public interface ServiceInstanceChooser { |
RibbonClientConfiguration
RibbonClientConfiguration是一个重要的配置类,它配置了以下接口的实例对象:
IClientConfig:DefaultClientConfigImplIRule:ZoneAvoidanceRuleIPing:DummyPingServerList:ConfigurationBasedServerListServerListUpdater:PollingServerListUpdaterILoadBalancer:ZoneAwareLoadBalancerServerListFilter:ZonePreferenceServerListFilterRetryHandler:DefaultLoadBalancerRetryHandler
RibbonLoadBalancerClient
上文我们知道了LoadBalancerClient的实现类是RibbonLoadBalancerClient,这个类是非常重要的一个类,最终的负载均衡由它来执行。
choose
现在我们来看看它是如何选择具体服务实例并获取到服务信息的。
1 | public ServiceInstance choose(String serviceId) { |
choose方法调用getServer去获取实例:
1 | protected Server getServer(String serviceId) { |
- 调用
getLoadBalancer()方法从Spring Context中获取ILoadBalancer的实例 - 然后在
getServer()方法中调用这个ILoadBalancer实例的chooseServer方法选择服务实例。传入的key固定为”default”。 - 新建一个
RibbonServer实例返回。
ILoadBalancer
ILoadBalancer在ribbon-loadbalancer包下,它是定义了实现软件负载均衡的一个接口,它需要一组可供选择的服务注册列表信息,以及根据特定方法区选择服务:
1 | public interface ILoadBalancer { |
ZoneAwareLoadBalancer
RibbonClientConfiguration配置类中新建了ILoadBalancer的实例ZoneAwareLoadBalancer:
1 |
|
这是一个比较核心的类。可以看到,ZoneAwareLoadBalancer新建的时候传入了RibbonClientConfiguration配置类中新建的各类参数:IClientConfig、IRule、IPing、ServerList、ServerListFilter、ServerListUpdater。
ZoneAwareLoadBalancer的继承关系如下:

跟踪ZoneAwareLoadBalancer的构造函数,由于ZoneAwareLoadBalancer继承了DynamicServerListLoadBalancer和BaseLoadBalancer,因此它会首先构造BaseLoadBalancer和DynamicServerListLoadBalancer。
DynamicServerListLoadBalancer的构造函数执行流程如下:
- 经过一系列的初始化配置
- 执行
restOfInit()方法 restOfInit()方法中,有一个updateListOfServers()方法,该方法用来获取所有的ServerListupdateListOfServers()方法首先调用serverListImpl.getUpdatedListOfServers()获取所有的服务列表。serverListImpl是ServerList接口的具体实现DomainExtractingServerList。DomainExtractingServerList.getUpdatedListOfServers方法调用DiscoveryEnableNIWSServerList.getUpdatedListOfServers方法DiscoveryEnableNIWSServerList.getUpdatedListOfServers方法最终调用obtainServersViaDiscovery()方法返回相应服务所有的服务实例信息。
updateListOfServers()方法最后调用updateAllServerList方法更新服务实例
DiscoveryEnabledNIWSServerList.obtainServersViaDiscovery
obtainServersViaDiscovery方法中首先通过eurekaClientProvider获取EurekaClient。eurekaClientProvider的实现类是DefaultListableBeanFactory#Jsr330DependencyProvider,它返回Spring容器类中相应类的实例。
EurekaClient的实现类为CloudEurekaClient,它具有服务注册、获取服务注册列表的等全部功能。调用其getInstancesByVipAddress方法返回相应服务所有的服务实例信息。
更新注册信息
前文我们看到,LoadBalancer从EurekaClient中获取服务信息,这里我们看看LoadBalancer如何判断服务的可用性,如何更新服务注册信息。
前面我们说到,ZoneAwareLoadBalancer继承了BaseLoadBalancer,它在实例化中也会执行BaseLoadBalancer的构造函数。
在BaseLoadBalancer的构造函数中调用setupPingTask()开启一个PingTask任务,代码如下:
1 | void setupPingTask() { |
默认情况下pingIntervalSeconds为10,即每10秒执行一次PingTask。
PingTask任务主体中新建一个Pinger对象,调用其runPinger方法,流程如下:
执行
results = pingerStrategy.pingServers(ping, allServers),获取服务的可用性。其中pingerStrategy的实际类型为BaseLoadBalancer#SerialPingStrategy。ping的类型为IPing,它有一个isAlive方法判断server是否可用,这里的实际类型为NIWSDiscoveryPing。SerialPingStrategy的pingServers方法中遍历所有的服务实例,调用NIWSDiscoveryPing.isAlive方法获取服务实例的可用性。NIWSDiscoveryPing.isAlive方法获取服务实例的状态,如果状态为UP,说明服务可用;否则该服务不可用。将可用的服务实例保存在
List<Server> upServerList中。- 如果服务可用性发生了改变,则调用
ServerStatusChangeListener.serverStatusChanged进行相应的通知。
PollingServerListUpdater
Server List的更新在PollingServerListUpdater定时任务中执行。它在RibbonClientConfiguration中新建:
1 |
|
它在定时任务中调用DynamicServerListLoadBalancer.updateListOfServers方法,从本地的缓存(virtualHostNameAppMap)中获取服务列表,最后调用updateAllServerList方法更新服务实例,保存在BaseLoadBalancer实例的allServerList变量中。
chooseServer
ZoneAwareLoadBalancer的chooseServer方法代码如下:
1 | public Server chooseServer(Object key) { |
调用getLoadBalancerStats()方法获取LoadBalancerStats,LoadBalancerStats类保存了LoadBalancer每个节点的操作特征和统计信息。然后调用getAvailableZones方法获取可用的Zone,
根据可用Zone的不同,chooseServer方法选择不同的执行流程。本地中我们配置了defaultZone: http://peer1:8751/eureka/,http://peer2:8752/eureka/,http://peer3:8753/eureka/,因此可用的Zone只有一个。我们先看看可用Zone只有一个的情况。
可用Zone只有一个
如果可用Zone只有一个,调用父类BaseLoadBalancer的chooseServer方法。
chooseServer方法中调用IRule的choose方法选择一个Server。
IRule
IRule用于复杂均衡的策略,它有三个方法:
1 | public interface IRule{ |
其中choose()是根据key来获取server,setLoadBalancer()和getLoadBalancer()是用来设置和获取ILoadBalancer的。
IRule有很多默认的实现类,这些实现类根据不同的算法和逻辑来处理负载均衡。Ribbon实现的IRule如下图所示:

- BeastAvailableRule
- ClientConfigEnableRoundRobinRule
- AvailabilityFilteringRule
- ZoneAvoidanceRule
- RoundRobinRule
- WeightedResponseTimeRule
- RetryRule
- RandomRule
ZoneAvoidanceRule
默认选择的ZoneAvoidanceRule,根据server的zone区域和可用性来轮询选择。其choose方法如下:
1 | public Server choose(Object key) { |
- 首先调用
getLoadBalancer()方法获取ZoneAwareLoadBalancer - 调用
getPredicate()方法获取CompositePredicate实例 - 调用
CompositePredicate的父类AbstractServerPredicate的chooseRoundRobinAfterFiltering方法。传入调用ZoneAwareLoadBalancer.getAllServers()方法获得的服务列表,该列表从BaseLoadBalancer实例的allServerList属性中获取。
chooseRoundRobinAfterFiltering方法如下:
1 | public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) { |
- 调用
getEligibleServers方法获取适合的Server列表,判断是否合适的方法为ZoneAvoidanceRule.apply方法 - 然后调用
incrementAndGetModulo方法获取Server的index,该方法就是在modulo范围内轮询获取下一个index - 最后通过index获取Server
可用Zone大于一个
修改eureka-consumer的application.yml:
1 | spring: |
如果可用Zone大于一个,流程如下:
- 调用
getLoadBalancerStats()方法获取LoadBalancerStats - 调用
ZoneAvoidanceRule.createSnapsot保存关于zone的快照 - 调用
ZoneAvoidance.getAvailableZones返回可用的Zones 如果可用的Zones数量小于刚才zone快照里保存的数量
- 调用
ZoneAvoidanceRule.randomChooseZone从可用的Zones中随机选择一个zone - 根据选择的zone,调用
getLoadBalancer方法获取BaseLoadBalancer - 调用
BaseLoadBalancer.chooseServer方法选择服务实例
- 调用
否则,和可用Zone只有一个的情况一样,调用父类
BaseLoadBalancer的chooseServer方法。
@LoadBalanced
LoadBalancerClient有一种更简单的使用方法:
1 |
|
在创建RestTemplatebean的时候加上@LoadBalanced注解,我们就可以直接使用restTemplate:
1 | ("/consumer") |
原理
Spring为加@LoadBalanced注解的方法加入了拦截器,其配置在LoadBalancerAutoConfiguration类中。
1 |
|
该类首先维护了一个被@LoadBalanced修饰的RestTemplate对象的List,在初始化过程中,通过调用customizer.customize(restTemplate)方法来给RestTemplate增加拦截器LoadBalancerInterceptor。
拦截器LoadBalancerInterceptor的拦截方法如下:
1 | public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, |
首先根据url获取服务名称,然后调用RibbonLoadBalancerClient.execute方法。
RibbonLoadBalancerClient.execute方法和choose方法非常类似:
1 | public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException { |
- 调用
getLoadBalancer()方法从Spring Context中获取ILoadBalancer的实例 - 然后在
getServer()方法中调用这个ILoadBalancer实例的chooseServer方法选择服务实例。传入的key固定为”default”。 - 新建一个
RibbonServer实例。 - 最后调用获得的服务并返回结果
http://blog.didispace.com/spring-cloud-starter-dalston-2-1/
https://blog.csdn.net/forezp/article/details/74820899