Spring Cloud微服务阅读随笔--第4章【客户端负载均衡:Spring Cloud Ribbon】
客户端负载均衡:Spring Cloud Ribbon
Spring Cloud Ribbon 是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST 模板请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon 虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过RibbonL来实现的,包括后续我们将要介绍的Fegin,它也是基于Ribbon实现的工具。所以,对Spring Cloud Ribbon的理解和使用,对于我们使用Spring Cloud来构建微服务非常重要。
在这一章中,我们将具体介绍如何使用Ribbon来实现客户端的负载均衡,并且通过源码分析来了解Ribbon实现客户端负载均衡的基本原理。
客户端负载均衡
负载均衡在系统架构中是一个非常重要,并且是不得不去实施的内容。因为负载均衡是对系统的高可用、网络压力的缓解和处理能力扩容的重要手段之一。我们通常所说的负载均衡都指的是服务端负载均衡,其中分为硬件负载均衡和软件负载均衡。硬件负载均衡主要通过在服务器节点之间安装专门用于负载均衡的设备,比如F5等:而软件负载均衡则是通过在服务器上安装一些具有均衡负载功能或模块的软件来完成请求的分发工作,比如Nginx等。不论采用硬件负载均衡还是软件负载均衡,只要是服务端负载均衡都以类似下图的架构方式构建起来:
【硬件负载均衡的设备】或是【软件负载均衡的软件模块】都会维护一个下挂可用的服务端清单,通过心跳检测来剔除故障的服务端节点以保证清单中都是可以正常访问的服务端节点。当客户端发送请求到负载均衡设备的时候,该设备按某种算法(比如线性轮询、按权重负载、按流量负载等)从维护的可用服务端清单中取出一台服务端的地址,然后进行转发。
而【客户端负载均衡】和【服务端负载均衡】最大的不同点在于上面所提到的服务清单所存储的位置。在客户端负载均衡中,所有客户端节点都维护着自己要访问的服务端清单,而这些服务端的清单来自于服务注册中心,比如上一章我们介绍的Eureka服务端。同服务端负载均衡的架构类似,在客户端负载均衡中也需要心跳去维护服务端清单的健康性,只是这个步骤需要与服务注册中心配合完成。在Spring Cloud实现的服务治理框架中,默认会创建针对各个服务治理框架的Ribbon自动化整合配置,比如Eureka 的org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,Consul中的org.springframework.cloud.consul.discovery.RibbonConsulAuto-Configuration。在实际使用的时候,我们可以通过查看这两个类的实现,以找到它们的配置详情来帮助我们更好地使用它。
通过Spring Cloud Ribbon的封装,我们在微服务架构中使用客户端负载均衡调用非常简单,只需要如下两步:
1. 服务提供者只需要启动多个服务实例并注册到一个注册中心或是多个相关联的服务注册中心。
2. 服务消费者直接通过调用被@LoadBalanced注解修饰过的RestTemplate来实现面对服务的接口调用。
这样,我们就可以将服务提供者的高可用以及服务消费者的负载均衡调用一起实现了。
RestTemplate详解
在上一章中,我们已经通过引入Ribbon实现了服务消费者的客户端负载均衡功能,可以通过查看第3章中的“服务发现与消费”一节来获取实验示例。其中,我们使用了一个非常有用的对象RestTemplate。该对象会使用Ribbon的自动化配置,同时通过配置@LoadBalanced还能够开启客户端负载均衡。之前我们演示了通过RestTemplate实现了最简单的服务访问,下面我们将详细介绍RestTemplate针对几种不同请求类型和参数类型的服务调用实现。
GET请求
在RestTemplate中,对GET请求可以通过如下两个方法进行调用实现。
第一种:getForEntity函数。该方法返回的是ResponseEntity,该对象是Spring 对HTTP请求响应的封装,其中主要存储了HTTP的几个重要元素,比如HTTP请求状态码的枚举对象HttpStatus(也就是我们常说的404、500这些错误码)、在它的父类HttpEntity中还存储着HTTP请求的头信息对象HttpHeaders以及泛型类型的请求体对象。比如下面的例子,就是访问USER-SERVER服务的/user 请求,同时最后一个参数didi 会替换url中的 {1}占位符,而返回的ResponseEntity对象中的body内容类型会根据第二个参数转换为String类型。
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://USER-SERVICE/user?name={1}",String.class,"didi");
String body = responseEntity.getBody();
若我们希望返回的body是一个User对象类型,也可以这样实现:
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<User> responseEntity = restTemplate.getForEntity("http://USER-SERVICE/user?name={1}",User.class,"didi");
User body = responseEntity.getBody();
上面的例子是比较常见的方法,getForEntity函数实际上提供了一下三种不同的重载实现。
1. getForEntity(String url,Class responseType,Object...urlVariables):该方法提供了三个参数,urlVariables为url中的参数绑定。GET请求的参数绑定通常使用url中拼接的方式,比如http://USER-SERVICE/user?name=didi,我们可以像这样自己讲参数拼接到url中,但更好的方法是在url中使用占位符并配合urlVariables参数实现GET请求的参数绑定,比如url定义为http://USER=SERVICE/user?name={1},然后可以这样来调用:getForEntity("http://USER-SERVICVE/user?name={1}",String.class,"didi"),其中第三个参数didi会替换url中的{1}占位符。这里需要注意的是,由于urlVariables参数是一个数组,所以它的顺序会对应url 中占位符定义的数字顺序。
2. getForEntity(String url,Class responseType,Map urlVariables):该方法提供的参数中,只有urlVariables的参数类型与上面的方法不同。这里使用了Map类型,所以使用该方法进行参数绑定时需要在占位符中指定Map中参数的key值,比如url定义为http://USER-SERVICE/user?name={name},在Map类型的urlVariables中,我们就需要put一个key为name的参数来绑定url中{name}占位符的值,比如:
RestTemplate restTemplate = new RestTemplate();
Map<String,String> params = new BashMap<>();
params.put("name","dada");
ResponseEntity<String> reponseEntity = restTemplate.getForEntity("http://USER-SERVICE/user?name={name}",String.class,params);
3. getForEntity(URI url,Class responseType):该方法使用URI对象来替代之前的url 和urlVariables参数来指定访问地址和参数绑定。URI是JDK java.net包下的一个类,它表示一个统一资源标识符(Uniform Resource Identifier)引用。比如下面的例子:
RestTemplate restTemplate = new RestTemplate();
UriComponents uriComponents = UriComponentsBuilder.formUriString("http://USER-SERVICE/user?name={name}")
.build()
.expand("dodo")
.encode();
URI uri = uriComponents.toUri();
ResponseEntity<String> responseEntity = restTemplate.getEntity(url,String.class.getBody());
更多关于如何定义一个URI的方法可以参见JDK文档,这里不做详细说明。
第二种:getForObject函数。该方法可以理解为对getForEntity的进一步封装,它通过HttpMessageConverterExtractor对HTTP的请求响应体body内容进行对象转换,实现请求直接返回包装好的对象内容。比如:
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject(uri,String.class);
当body是一个User对象时,可以直接这样实现:
RestTemplate restTemplate = new RestTemplate();
User result = restTemplate.getForObject(uri,User.class);
当不需要关注请求响应除body外的其他内容时,该函数就非常好用,可以少一个从Response中获取body的步骤。它与getForEntity函数类似,也提供了三种不同的重载实现。
1. getForObject(String url,Class responseType,Object ... urlVariables):与getForEntity的方法类似,url参数指定访问的地址,responseType参数定义该方法的返回类型,urlVariables参数为url中占位符对应的参数。
2. getForObject(String url,Class responseType,Map urlVariables):在该函数中,使用Map类型的urlVariables替代上面数组形式的urlVariables,因此使用时在url中需要将占位符的名称与Map类型中的key一一对应设置。
3. getForObject(URI url,Class responseType):该方法使用URI对象来替代之前的url 和urlVariables参数使用。
POST请求
在RestTemplate中,对POST请求时可以通过如下三个方法进行调用实现。
第一种:postForEntity函数。该方法同GET请求中的getForEntity类似,会在调用后返回responseEntity<T>对象,其中T为请求响应的body类型。比如下面这个例子,使用postForEntity提交POST请求到USER-SERVICE服务的/user接口,提交的body内容为user对象,请求响应返回的body类型为String。
RestTemplate restTemplate =new RestTemplate();
User user =new User("didi",30);
ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://USER-SERVICE/user",user,String.class);
String body = responseEntity.getBody();
postForEntity函数也实现了三种不同的重载方法。
1. postForEntity(String url,Object request,Class responseType,Object...uriVariables)
2. postForEntity(String url,Object request,Class responseType,Map uriVariables)
3. postForEntity(URI url,Object request,Class responseType)
这些函数中的参数用法大部分与getForEntity一致,比如,第一个重载函数和第二个重载函数中的UriVariables参数都用来对url中的参数进行绑定使用:responseType参数是对请求响应的body内容的类型定义。这里需要注意的是新增加的request参数,该参数可以是一个普通对象,也可以是一个HttpEntity对象。如果是一个普通对象,而非HttpEntity对象的时候,RestTemplate会将请求对象转换为一个HttpEntity对象来处理,其中Object就是request的类型,request内容会被视作完整的body来处理;而如果request是一个HttpEntity对象,那么就会被当作一个完成的HTTP请求对象来处理,这个request中不仅包含了body的内容,也包含了header的内容。
第二种:postForObject函数。该方法也跟getForObject的类型类似,它的作用是简化postForEntity的后续处理。通过直接将请求响应的body内容包装成对象来返回使用,比如下面的例子:
RestTemplate restTemplate = new RestTemplate();
User user = new User("didi",20);
String postResult = restTemplate.postForObject("http://USER-SERVICE/user",user,String.class);
postForObject函数也实现了三种不同的重载方法:
1. postForObject(String url,Object request,Class responseType,Object...urlVariables)
2. postForObject(String url,Object request,Class responseType,Map uriVariables)
3. postForObject(URI url,Object request,Class responseType)
这三个函数除了返回的对象类型不同,函数的传入参数均与postForEntity一致,因此可参考之前postForEntity的说明。
第三种:postForLocation函数。该方法实现了以POST请求提交资源,并返回新资源的URI,比如下面的例子:
User user = new User("didi",40);
URI responseURI = restTemplate.postForLocation("http://USER-SERVICE/user",user);
postForLocation函数也实现了三种不同的重载方法:
1. postForLocation(String url,Object request,Object...urlVariables)
2. postForLocation(String url,Object request,Map urlVariables)
3. postForLocation(URI url,Object request)
由于postForLocation函数会返回新资源的URI,该URI就相当于指定了返回类型,所以此方法实现的POST请求不需要像postForEntity和postForObject那样指定responseType。其他的参数用法相同。
PUT请求
在RestTemplate中,对PUT请求可以通过put方法进行调用实现,比如:
RestTemplate restTemplate = new RestTemplate();
Long id = 1000L;
User user = new User("didi",40);
restTemplate.put("http://USER-SERVICE/user/{1}",user,id)
put函数也实现了三种不同的重载方法:
1. put(String url,Object request,Object... urlVariables)
2. put(String url,Object request,Map urlVariables)
3. put(URI url,Object request)
put函数为void类型,所以没有返回内容,也就没有其他函数定义的responseType参数,除此之外的其他传入参数定义与用法与postForObject基本一致。
DELETE请求
在RestTemplate中,对DELATE请求可以通过delete方法进行调用实现,比如:
RestTemplate restTemplate = new RestTemplate();
Long id = 10001L;
restTemplate.delete("http://USER-SERVICE/user/{1}",id);
delete函数也实现了三种不同的重载方法:
1. delete(String url,Object... urlVariables)
2. delete(String url,Map urlVariables)
3. delete(URI url)
由于我们在进行REST请求时,通常都将DELETE请求的唯一标识拼接在url中,所以DELETE请求也不需要request的body信息,就如上面的三个函数实现一样,非常简单。url指定DELETE请求的位置,urlVariables绑定url中的参数即可。
源码分析
熟悉Spring的读者看到这里可能产生这样的疑问:RestTemplate不是Spring自己就能提供的吗?跟Ribbon的客户端负载均衡又有什么关系呢?在本节中,我们将透过现象看本质。探索一下Ribbon是如何通过RestTemplate实现客户端负载均衡的。
首先,回顾一下之前的消费者实例:我们是如何实现客户端负载均衡的?仔细观察一下之前的实现代码,可以发现在消费者的例子中,可能就@LoadBalanced这个注解是之前没有接触过的,并且从命名上来看也与负载均衡相关。我们不妨以此为线索来看看Spring Cloud Ribbon的源码实现。
从@LoadBalanced注解源码的注释中可以知道,该注解用来给RestTemplate做标记,以使用负载均衡的客户端(LoadBalancerClient)来配置它。
通过搜索LoadBalancerClient可以发现,这是Spring Cloud中定义的一个接口:
public interface LoadBalancerClient{
ServiceInstance choose (String serviceId);
<T>T execute (String serviced,LoadBalancerRequest<T> request) throws IOException;
URI reconstructURI(ServiceInstance instance,URI original);
}
从该接口中,我们可以通过定义的抽象方法来了解客户端负载均衡中应具备的几种能力。
1. ServiceInstance choose(String serviceId):根据传入的服务名serviceId,从负载均衡器中挑选一个对应服务的实例。
2. T execute(String serviceId,LoadBalancerRequest request) throws IOException:使用从负载均衡器中挑选出的服务实例来执行请求内容。
3. URI reconstructURI(ServiceInstance instance,URI original):为系统构建一个合适的 host:port 形式的URI。在分布式系统中,我们使用逻辑上的服务名称作为host 来构建URI(替代服务实例的host:port形式)进行请求,比如 http://myservice/path/to/service。在该操作的定义中,前者 ServiceInstance 对象是带有 host 和port 的具体服务实例,而后者URI 对象则是使用逻辑服务名定义为 host 的URI,而返回的 URI 内容则是通过 ServiceInstance 的服务实例详情拼接出的具体的 host:port 形式的请求地址。
顺着LoadBalancerClient接口的所属包org.springframework.cloud.client.loadbalancer,我们将其内容进行整理,可以得出如下图所示的关系。
从类的命名上可初步判断LoadBalancerAutoConfiguration为实现客户端负载均衡器的自动化配置类。通过查看源码,我们可以验证这一点假设:
@Configuration
@ConditionalOnClass(RestTemplate.class) // 当给定的类名在类路径上存在,则实例化当前Bean
@ConditionalOnBean(LoadBalancerClient.class) // 当给定的在bean存在时,则实例化当前Bean
public class LoadBalancerAutoConfiguration {
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Bean
public SmartinitializingSingleton loadBalancedRestTemplateinitializer(
final List<RestTemplateCustomizer>customizers) {
return new SmartinitializingSingleton() {
@Override
public void afterSingletonsinstantiated () {
for (RestTemplaterestTemplate : LoadBalancerAutoConfiguration.this.restTemplates ){
for (RestTemplateCustomizer customizer:customizers) {
customizer.customize(restTemplate);
}
}
}
};
}
@Bean
@ConditionalOnMissingBean // 当给定的类名在类路径上不存在,则实例化当前Bean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerinterceptor loadBalancerinterceptor) {
return new RestTemplateCustomizer() {
@Override
public void customize(RestTemplate restTemplate) {
List<ClientHttpRequestinterceptor> list= new ArrayList<>(
rest Template. get Interceptors()); list.add(loadBalancerinterceptor);
restTemplate.setinterceptors(list);
}
};
}
@Bean
public LoadBalancerinterceptor ribboninterceptor(
LoadBalancerClient loadBalancerClient) {
return new LoadBalancerinterceptor(loadBalancerClient);
}
}
从LoadBalancerAutoConfiguration类头上的注解可以知道,Ribbon实现的负载均衡自动化配置需要满足下面两个条件。
1. @ConditionalOnClass(RestTemplate.class):RestTemplate类必须存在于当前工程的环境中。
2. @ConditionalOnBean(LoadBalancerClient.class):在Spring的Bean工程中必须有LoadBalancerClient的实现Bean。
在该自动化配置类,主要做了下面三件事:
1. 创建了一个LoadBalancerInterceptor的Bean,用于实现对客户端发起请求时拦截,以实现客户端负载均衡。
2. 创建了一个RestTemplateCutomizer的Bean,用于给RestTemplate增加LoadBalancerInterceptor拦截器。
3. 维护了一个被@LoadBalanced注解修饰的RestTemplate对象列表,并在这里进行初始化,通过调用RestTemplateCustomizer的实例来给需要客户端负载均衡的RestTemplate增加LoadBalancerInterceptor拦截器。
接下来,我们看看LoadBalancerInterceptor拦截器是如何将一个普通的RestTemplate变成客户端负载均衡的:
package org.springframework.cloud.client.loadbalancer; import java.io.IOException; import java.net.URI; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; import org.springframework.cloud.client.loadbalancer.LoadBalancerRequestFactory; import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import org.springframework.util.Assert; public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor { private LoadBalancerClient loadBalancer; private LoadBalancerRequestFactory requestFactory; public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) { this.loadBalancer = loadBalancer; this.requestFactory = requestFactory; } public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) { this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer)); } public ClientHttpResponse intercept(HttpRequest request, byte[] body, 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)); } }
通过源码以及之前的自动化配置类,我们可以看到在拦截器中注入了LoadBalancerClient的实现。当一个被@LoadBalanced注解修饰的RestTemplate对象向外发起HTTP请求时,会被LoadBalancerInterceptor类的intercept函数所拦截。由于我们在使用RestTemplate时采用了服务名作为host,所以直接从HttpRequest的URI对象中通过getHost()就可以拿到服务名,然后调用execute函数去根据服务名来选择实例并发起实际的的请求。
分析到这里,LoadBanlancerClient还只是一个抽象的负载均衡接口,所以我们还需要找到它的具体实现类来进行分析。通过查看Ribbon的源码,可以很容易地在org.springframework.cloud.netfilix.ribbon包下找到对应的实现类Ribbon-LoadBalancerClient。
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException { ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId); Server server = this.getServer(loadBalancer); if(server == null) { throw new IllegalStateException("No instances available for " + serviceId); } else { RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server)); return this.execute(serviceId, ribbonServer, request); } } public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException { Server server = null; if(serviceInstance instanceof RibbonLoadBalancerClient.RibbonServer) { server = ((RibbonLoadBalancerClient.RibbonServer)serviceInstance).getServer(); } if(server == null) { throw new IllegalStateException("No instances available for " + serviceId); } else { RibbonLoadBalancerContext context = this.clientFactory.getLoadBalancerContext(serviceId); RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server); try { Object ex = request.apply(serviceInstance); statsRecorder.recordStats(ex); return ex; } catch (IOException var8) { statsRecorder.recordStats(var8); throw var8; } catch (Exception var9) { statsRecorder.recordStats(var9); ReflectionUtils.rethrowRuntimeException(var9); return null; } } }
可以看到,在execute函数的实现中,第一步做的就是通过getServer根据传入的服务名serviceId去获得具体的服务实例:
protected Server getServer(ILoadBalancer load.Balancer) { if (load.Balancer == null) { return null;
} return loadBalancer.chooseServer("default"); 、
}
通过getServer函数的实现源码,我们可以看到这里获取具体服务实例的时候并没有使用LoadBalancerClient接口中的choose函数,而是使用了Netfix Ribbon自身的ILoadBalancer接口中定义的chooseServer函数。
package com.netflix.loadbalancer; import com.netflix.loadbalancer.Server; import java.util.List; public interface ILoadBalancer { void addServers(List<Server> var1); Server chooseServer(Object var1); void markServerDown(Server var1); /** @deprecated */ @Deprecated List<Server> getServerList(boolean var1); List<Server> getReachableServers(); List<Server> getAllServers(); }
可以看到,在该接口中定义了一个客户端负载均衡器需要的一系列抽象操作(未列举过期函数)。
1. addServers:向负载均衡器中维护的实例列表增加服务实例。
2. chooseServer:通过某种策略,从负载均衡器中挑选出一个具体的服务实例。
3. markServerDown:用来通知和标识负载均衡中某个具体实例已经停止服务,不然负载均衡器在下一次获取服务实例清单前都会认为服务实例均是正常服务的。
4. getReachableServers:获取当前正常服务的实例列表。
5. getAllServers:获取所有一直的服务实例列表,包括正常服务和停止服务的实例。
在该接口定义中涉及的Server对象定义是一个传统的服务端节点,在该类中存储了服务节点的一些元数据信息,包括host、port以及一些部署信息等。
而对于该接口的实现,我们整理出如下图所示的结构。可以看到,BaseLoadBalancer类实现了基础的负载均衡,而DynameicServerListLoadBalancer和ZoneAwareLoadBalancer在负载均衡的策略上做了些功能的扩展。
那么在这整合Ribbon的时候Spring Cloud默认采用了那个具体实现呢?我们通过RibbonClientConfiguration配置类,可以知道在整合时默认采用了ZoneAware-LoadBalancer来实现负载均衡器。
@Bean @ConditionalOnMissingBean public !Load.Balancer ribbonLoad.Balancer(IC让entConfig config, ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,IRule rule, IPing ping) {
ZoneAwareLoad.Balancer<Server> balancer = LoadBalancerBuilder.newBuilder() .withClientConfig(config) .withRule(rule) .withPing(ping) .withServerListFilter(serverListFilter).withDynamicServerList(serverList) .buildDynamicServerListLoad.Balancer(); return balancer;
}
下面,我们在回到RibbonLoadBalancerClient的execute函数逻辑,在通过ZoneAwareLoadBalancer的chooseServer函数获取了负载均衡策略分配到的服务实例对象Server之后,将其内容包装成RibbonServer对象(该对象除了存储了服务实例的信息之外,还增加了服务名serviceId、是否需要使用HTTPS等其他信息),然后使用该对象再回调LoadBalancerInterceptor请求拦截器中LoadBalancerRequest的apply(final ServiceInstance instance)函数,像一个实际的具体服务实例发起请求,从而实现一开始以服务名为host的URI请求到host:post形式的实际访问地址的转换。
在apply(final ServiceInstance instance)函数中传入的ServiceIntance接口对象是对服务实例的抽象定义。在该接口中暴露了服务治理系统中每个服务实例需要提供的一些基本信息,比如serviceId、host、port等,具体定义如下:
public interface ServiceInstance { String getServiceId(); String getHost(); int getPort(); boolean isSecure(); URI getUri(); Map<String, String> getMetadata(); }
而上面提到的具体包装Server服务实例的RibbonServer对象就是ServiceInstance接口的实现,可以看到它除了包含Server对象之外,还存储了服务名、是否使用HTTPS标识以及一个Map类型的元数据集合。
protected static class RibbonServer implements Serviceinstance { private final String serviceid; private final Server server; private final boolean secure; private Map<String, String> metadata;
protected RibbonServer(String serviceId, Server server) { this(serviceId, server, false, Calleetions.<String, String> emptyMap());
}
protected RibbonServer(String serviceid, Server server, boolean secure, Map<string, string=""> metadata) {
this.serviceId = serviceId;
this.server = server;
this.secure = secure;
this.metadata = metadata
}
//省略实现 ServiceInstance的一些获取Server信息的get函数
...
}
那么apply(final ServiceInstance instance)函数在接收到了具体ServiceInstance实例后,是如何通过LoadBalancerClient接口中的reconstructURI操作来组织具体请求地址的呢?
@Override public ClientHttpResponse apply(final Serviceinstance instance) throws Exception ( HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance); return execution.execute(serviceRequest, body);
}
从apply的实现中,可以看到它具体执行的时候,还传入了ServiceRequest-Wrapper对象,该对象继承了HttpRequestWrapper并重写了getURI函数,重写后的getURI通过LoadBalancerClient接口的reconstructURI函数来重新构建一个URI来进行访问。
private class ServiceRequestWrapper extends HttpRequestWrapper { private final Serviceinstance instance;
...
@Override public URI getURI() { URI uri = LoadBalancerInterceptor.this.loadBalancer.reconstructURI( this.instance, getRequest() .getURI{)); return uri;
}
}
在LoadBalancerInterceptor拦截器中,ClientHttpRequestExecution的实例具体执行execution.executie(serviceRequest,body)时,会调用Intercepting-ClientHttpRequest 下InterceptingRequestExecution类的execute函数,具体实现如下:
public ClientHttpResponse execute{HttpRequest request, byte[] body) throws IOException { if (this.iterator.hasNext()) { ClientHttpRequestinterceptor nextInterceptor = this.iterator.next(); return nextinterceptor.intercept(request, body, this);
}else{
ClientHttpRequest delegate = requestFactory. createRequest {request. get URI {) , request. getMethod {));
delegate.getHeaders {) .putAll (request.getHeaders {));
if (body.length > 0) {
StreamUtils.copy(body, delegate.getBody());
}
return delegate.execute();
}
}
可以看到,在创建请求的时候requestFactory.createRequest(request.getURI(),request.getMethod());这里的request.getURI()会调用之前介绍的ServiceRequestWrapper对象中重写的getURI函数。此时,它就会使用RibbonLoadBalancerClient中实现的reconstructURI来组织具体请求的服务实例地址。
public URI reconstructURI(Serviceinstance instance, URI original) { Assert.notNull(instance, "instance can not be null"); String serviceid = instance.getServiceid(); RibbonLoadBalancerContext context = this.clientFactory .getLoadBalancerContext(serviceid);
Server server = new Server (instance. getHost () , instance. get Port()) ; boolean secure = isSecure(server, serviceid); URI uri = original; if (secure) { uri = UriComponentsBuilder. fromUri (uri) . scheme ("https") .build() . toUri ();
}
return context.reconstructURIWithServer(server, uri);
}
从reconstructURI函数中我们可以看到,它通过ServiceInstance实例对象的serviceId,从SpringClientFactory类的clientFactory对象中获取对应的serviceId的负载均衡器的上下文RibbonLoadBalancerContext对象。然后根据ServiceInstance中的信息来构建具体服务实例信息的Server对象,并使用RibbonLoadBalancerContext独享的reconstructURIWithServer函数来构建服务实例的URI。
为了帮助理解,简单介绍一下上面提到的SpringClientFactory和RibbonLoad-BalancerContext:
1. SpringClientFactory类是一个用来创建客户端负载均衡器的工厂类,该工厂类会为每一个不同名的Ribbon客户端生成不同的Spring上下文。
2. RibbonLoadBalancerContext类是LoadBalancerContext的子类,该类用于存储一些被负载均衡器使用的上下文内容和API操作(reconstruct-URIWithServer就是其中之一)
从reconstructURIWithServer的实现中我们可以看到,它同reconstructURI的定义类似。只是resonstructURI的第一个保存具体服务实例的参数使用了Spring Cloud定义的ServiceInstance,而reconstructURIWithServer中使用了Netflix中定义的Server,所以在RibbonLoadBalancerClient实现reconstructURI的时候,做一次转换,使用ServiceInstance的host和port信息构建了一个Server对象来给reconstructURIWithServer使用。从reconstructURIWithServer的实现逻辑中,我们可以看到,它从Server对象中获取host和port信息,然后根据以服务名为host的URI对象original中获取其他请求信息,将两者内容进行拼接整合,形成最终要访问的服务实例的具体地址。
public class LoadBalancerContext implements IClientConfigAware { ... public URI reconstructURIWithServer(Server server, URI original) { String host = server.getHost(); int port = server .getPort(); if (host. equals (original. getHost ()) && port == original. get Port()) { return original; } String scheme = original.getScheme(); if (scheme == null) { scheme = deriveSchemeAndPortFromPartialUri (original) . first(); } try { StringBuilder sb = new StringBuilder(); sb.append(scheme).append("://"); if (!Strings. isNullOrEmpty (original. getRawUserinfo ())) { sb.append(original.getRawUserinfo()) .append("@"); } sb.append(host); if (port >= 0) { sb. append (" : ") . append (port) ; } sb. append (original. getRawPath ()) ; if (!Strings.isNullOrEmpty(original.getRawQuery())) { sb. append ("?") . append (original. getRawQuery ()); } if(!Strings.isNullOrEmpty(original.getRawFragment())) { sb.append("#") .append(original.getRawFragment()); } URI newURI = new URI(sb.toString()); return newURI; }catch (URISyntaxException e){ throw new RuntimeException(e); } } }
另外,从RibbonLoadBalancerClient的execute函数逻辑中,我们还能看到在回调拦截器中,执行具体的请求之后,Ribbon还通过RibbonStatsRecorder对象对服务请求进行了跟踪记录,这里不再展开说明,有兴趣可以继续研究。
分析到这里,我们已经可以大致理清Spring Cloud Ribbon中实现客户端负载均衡器的基本脉络,了解了它是如何通过LoadBalancerInterceptor拦截器对RestTemplate的请求进行拦截,并利用Spring Cloud的负载均衡器LoadBalancerClient将以逻辑服务名为host的URI转换成具体的服务实例地址的过程。同时通过分析LoadBalancerClient的Ribbon实现RibbonLoadBalancerClient,可以知道在使用Ribbon实现负载均衡器的时候,实际使用的还是Ribbon中定义的ILoadBalancer接口的实现,自动化配置会采用ZoneAwareLoadBalancer的实例来实现客户端负载均衡。
负载均衡器
通过之前的分析,我们已经对Spring Cloud如何使用Ribbon有了基本的了解。虽然Spring Cloud中定义了LoadBalancerClient作为负载均衡器的通用接口,并且针对Ribbon实现了RibbonLoadBalancerClient,但是它在具体实现客户端负载均衡时,是通过RIbbon的ILoadBalancer接口实现的。在上一节进行分析时候,我们对该接口的实现结构已经做了一些简单的介绍,下面我们根据ILoadBalancer接口的实现类逐个看看它是如何实现客户端负载均衡的。
AbstractLoadBalancer
AbstractLoadBalancer是ILoadBalancer接口的抽象实现。在该抽象类中定义了一个关于服务实例的分组枚举类ServerGroup,它包含以下三种不同类型。
1. ALL:所有服务实例。
2. STATUS_UP:正常服务的实例。
3. STATUS_NOT_UP:停止服务的实例。
另外,还实现了一个chooseServer()函数,该函数通过调用接口中的chooseServer(Object key)实现,其中参数key为null,表示在选择具体服务实例时忽略key的条件判断。
最后,还定义了两个抽象函数。
1. getServerList(ServerGroup serverGroup):定义了根据分组类型来获取不同的服务实例的列表。
2. getLoadBalancerStats():定义了获取LoadBalancerStats对象的方法,LoadBalancerStats对象被用来存储负载均衡器中各个服务实例当前的属性和统计信息。这些信息非常有用,我们可以利用这些信息来观察负载均衡器的运行情况,同时这些信息也是用来指定负载均衡策略的重要依据。
public abstract class AbstractLoadBalancer implements ILoadBalancer { public enum ServerGroup{ ALL, STATUS UP, STATUS NOT UP }
public Server chooseServer() { return chooseServer(null); }
public abstract List<Server> getServerList(ServerGroup serverGroup);
public abstract LoadBalancerStats getLoadBalancerStats(}; }
BasetLoadBalancer
BaseLoadBalancer类是Ribbon负载均衡器的基础实现类,在该类中定义了很多关于负载均衡器相关的基础内容。
1. 定义并维护了两个存储服务实例Server对象的列表。一个用于存储所有服务实例的清单,一个用于存储正常服务的实例清单。
@Monitor(name= PREFIX +"AllServerList",type=DataSourceType.INFORMATIONAL) protected volatile List<Server> allServerList = Collections .synchronizedList(new ArrayList<Server>()); @Monitor(name= PREFIX +"UpServerList",type=DataSourceType.INFORMATIONAL) protected volatile List<Server> upServerList = Collections .synchronizedList(new ArrayList<Server>());
2. 定义了之前我们提到的用来存储负载器各服务实例属性和统计信息的LoadBalancerStats对象。
3. 定义了检查服务实例是否正常服务的IPing对象,在BaseLoadBalancer中默认为null,需要在构造时注入它的具体实现。
4. 定义了检查服务实例操作的执行策略对象IPingStrategy,在BaseLoadBalancer中默认使用了该类中定义的静态内部类SerialPingStrategy实现。根据源码,我们可以看到该策略采用线性遍历ping服务实例的方式实现检查。该策略在当IPing的实现速度不理想,或是Server列表过大时,可能会影响系统性能,这时候需要通过实现IPingStrategy接口并重写pingServers(IPing ping,Server[] servers)函数去扩展ping的执行策略。
private static class SerialPingStrategy implements IPingStrategy { @Override public boolean[] pingServers(IPing ping, Server[) servers) { int numCandidates = servers.length;
boolean[] results = new boolean[numCandidates];
if (logger. isDebugEnabled ()) { logger. debug ("LoadBalancer: PingTask executing [" + numCandidates +") servers configured"); } for (int i= 0; i < numCandida七es; i++) { results[i] = false; try { if (ping ! = null) { results[i] = ping.isAlive(servers[i]); } }catch (Throwable t){ logger.error("Exception while pinging Server:" + servers [il, t); } } return results; } }
5. 定义了负载均衡的处理规则IRule对象,从BaseLoadBalancer中chooseServer(Object key)的实现源码,我们知道,负载均衡器实际将服务实例选择任务委托给了IRule实例中的choose函数来实现。而在这里,默认初始化了RoundRobinRule为IRule的实现对象。RoundRobinRule实现了最基本且常用的线性负载均衡规则。
public Server chooseServer(Object key){ if(counter == null){ counter = createCounter(); } counter.increment();
if(rule == null){ return null; }else{ try{ return rule.choose(key); }catch(Throwable t){ return null; } } }
6. 启动ping任务:在BaseLoadBalancer的默认构造函数中,会直接启动一个用于定时检查Server是否健康的任务。该任务默认的执行间隔为10秒。
7. 实现了ILoadBalacer接口定义的负载均衡器应具备以下一系列基本操作。
a. addServers(List newServers):向负载均衡器中增加新的服务实例列表,该实现将原本已经维护着的所有服务实例清单allServerList和新传入的服务实例清单newServers都加入到newList中,然后通过调用setServersList函数对newList 进行处理,在BaseLoadBalancer中实现的时候会使用新的列表覆盖旧的列表。而之后介绍的几个扩展实现类对于服务实例清单更新的优化都是通过对setServersList函数的重写来实现的。
public void addServers(List<Server> newServers} { if (newServers != null && newServers.size() > 0) { try { ArrayList<Server> newList = new ArrayList<Server>(); newList.addAll(allServerList); newList.addAll(newServers); setServersList(newList}; } catch (Exception e) { logger.error("Exception while adding Servers", e); } } }
8. chooseServer(Object key):挑选一个具体的服务实例,在上面介绍IRule的时候,已经做了说明,这里不再赘述。
9. markServerDown(Server server):标记某个服务实例暂停服务。
public void markServerDown(Server server){ if(server == null){ return; } if(!server.isAlive()){ return; } logger.error("LoadBalancer:markServerDown called on["+ server.getId()+"]"); server.setAlive(false); notifyServerStatusChangeListener(singleton(server)); }
10. getReachableServers():获取可用的服务实例列表。由于BaseLoadBalancer中单独维护了一个正常服务的实例清单,所以直接返回即可。
public List<Server> getReachableServers() ( return Collections.unmodifiableList(upServerList); }
11. getAllServers():获取所有的服务实例列表。由于BaseLoadBalancer中单独维护了一个所有服务的实例清单,所以也直接返回它即可。
public List<Server> getA11Servers() { return Collec七ions.unmodifiableList(allServerList); }
DynamicServerListLoadBalancer
DynamicServerListLoadBanlancer类继承于BaseLoadBalancer类,它是对基础负载均衡器的扩展。在该负载均衡器中,实现了服务实例清单在运行期的状态更新能力;同时,它还具备了对服务实例清单的过滤功能,也就是说,我们可以通过过滤器来选择性地获取一批服务实例清单。下面我们具体来看看在该类中增加了一些什么内容。
ServerList
从DynamicServerListLoadBalancer的成员定义中,我们可以发现新增了一个关于服务列表的操作对象ServerList<T> serverListImpl。其中泛型T从类名中对于T的限定DynamicServerListLoadBalancer<T exetends Server>可以获知它是一个Server的子类,即代表了一个具体的服务实例的扩展类。而ServerList接口定义如下所示:
public interface ServerList<T extends Server> { public List<T> getInitialListOfServers () ; public List<T> getUpdatedListOfServers () ; }
它定义了两个抽象方法:getInitialListOfServers用于获取初始化的服务实例清单,而getUpdateListOfServers用于获取更新的服务实例清单。那么该接口的实现有哪些呢?通过搜索源码,我们可以整理出如下图所示的结构。
从上图中我们可以看到有多个ServerList的实现类,那么在DynamicServer-ListLoadBalancer中的ServerList默认配置到底使用了哪个具体实现呢?既然在该负载均衡器中需要实现服务实例的动态更新,那么势必需要Ribbon具体访问Eureka来获取服务实例的能力,所以我们从Spring Cloud整合Ribbon与Eureka的包org.springframework.cloud.netflix.ribbon.eureka下进行探索,可以找到配置类EurekaRibbonClientConfiguration,在该类中可以找到如下创建ServerList实例的内容:
@Bean @ConditionalOnMissingBean public ServerList<?> ribbonServerList(IClientConfig config) {
DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(config);
DomainExtractingServerList serverList = new DomainExtractingServerList( discoveryServerList, config, this.approximateZoneFromHostname);
return serverList; }
可以看到,这里创建的是一个DomainExtractingServerList实例,从下面它的源码中我们可以看到,在它内部还定义了一个ServerList list。同时,Domain-ExtractingServerList类中getInitialListOfServers和getUpdated-ListOfServers的具体实现,其实委托给了内部定义的ServerList list对象,而该对象是通过创建DomainExtractingServerList时,由构造函数传入的DiscveryEnabledNIWSServerList实现的。
public class DomainExtractingServerList implements ServerList<DiscoveryEnabledServer> { private ServerList<DiscoveryEnabledServer> list; private IClientConfig clientConfig; private boolean approximateZoneFromHostname;
public DomainExtractingServerList(ServerList<DiscoveryEnabledServer> list, IClientConfig clientConfig, boolean approximateZoneFromHostname) { this. list = list; this.clientConfig = clientConfig; this.approximateZoneFromHostname = approximateZoneFromHostname; }
@Override public List<DiscoveryEnabledServer> getinitialListOfServers () { List<DiscoveryEnabledServer> servers = setZones(this.list .getinitialListOfServers()); return servers; }
@Override public List<DiscoveryEnabledServer> getUpdatedListOfServers () { List<DiscoveryEnabledServer> servers = setZones(this.list .getUpdatedListOfServers()); return servers; } ... }
那么DiscoveryEnabledNIWSServerList是如何实现这两个服务实例获取的呢?我们从源码中可以看到这两个方法都是通过该类中的一个私有函数obtainServersDiscovery通过服务发现机制来实现服务实例的获取的。
@Override public List<DiscoveryEnabledServer> getinitialListOfServers() { return obtainServersViaDiscovery(); }
@Override public List<DiscoveryEnabledServer> getUpdatedListOfServers {) { return obtainServersViaDiscovery{); }
而obtainServersDiscovery的实现逻辑如下所示,主要依靠EurekaClient从服务注册中心获取到具体的服务实例InstanceInfo列表(EurekaClient的具体实现,我们在分析Eureka的源码时已经做了详细的介绍,这里传入的vipAddress可以理解为逻辑上的服务名,比如USER-SERVICE)。接着,对这些服务实例进行遍历,将状态为UP(正常服务)的实例转换成DiscoveryEnabledServer对象,最后讲这些实例组织成列表返回。
private List<DiscoveryEnabledServer> obtainServersViaDiscovery() { List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer> (); if (eurekaClientProvider== null || eurekaClientProvider. get() == null) { logger.warn("EurekaClient has not been initialized yet, returning an empty list"); return new ArrayList<DiscoveryEnabledServer>(); }
EurekaClient eurekaClient = eurekaClientProvider.get(); if (vipAddresses != null) { for (String vipAddress : vipAddresses. split (", ")) { List<Instanceinfo> listOfInstanceinfo = eurekaClient.getInstancesByVipAddress( vipAddress, isSecure, targetRegion); for (Instanceinfo ii : listOfInstanceinfo) { if(ii.getStatus () . equals (InstanceStatus. UP)) { //省略了 一些实例信息的加工逻辑 DiscoveryEnabledServer des= new DiscoveryEnabledServer(ii,isSecure, shouldUseipAddr); des.setZone(DiscoveryClient.getZone (ii)); serverList.add(des); } } if (serverList.size()>O && prioritizeVipAddressBasedServers) { break; } } } return serverList; }
在DiscoveryEnableNIWSServerList中通过EurekaClent从服务注册中心获取到最新的服务实例清单后,返回的List到了DomainExtractingServerList类中,将继续通过setZones函数进行处理。而这里的处理具体内容如下所示,主要完成将DiscoveryEnabledNIWSServerList返回的List列表中的元素,转换成内部定义的DiscoveryEnabledServer的子类对象DomainExtractingServer,在该对象的构造函数中将为服务实例对象设置一些必要的属性,比如id、zone、isAliveFlag、readyToserve等信息。
private List<DiscoveryEnabledServer> setZones(List<DiscoveryEnabledServer> servers) { List<DiscoveryEnabledServer> result = new ArrayList<>();
boolean isSecure = this.clientConfig.getPropertyAsBoolean( CommonClientConfigKey.IsSecure, Boolean.TRUE); boolean shouldUseipAddr = this.clientConfig.getPropertyAsBoolean( CommonClientConfigKey.UseIPAddrForServer, Boolean.FALSE);
for (DiscoveryEnabledServer server : servers) { result.add(new DomainExtractingServer(server, isSecure, shouldUse!pAddr, this.approximateZoneFromHostname)); } return result; }
ServerListUpdater
通过上面的分析我们已经知道了Ribbon与Eureka整合后,如何实现从Eureka Server中获取服务实例清单。那么它又是如何触发向Eureka Server去获取服务实例清单以及如何在获取到服务实例清单后更新本地的服务实例清单的呢?继续来看DynamicServer-ListLoadBalancer中的实现内容,我们可以很容易地找到下面定义的关于ServerListUpdater的内容:
protected final ServerListUpdater.UpdateAction updateAction = new Server ListUpdater.UpdateAction() { @Override public void doUpdate() { updateListOfServers(); } }; protected volatile ServerListUpdater serverListUpdater;
根据该接口的命名,我们基本就能猜到,这个对象实现的是对ServerList的更新,所以可以称它为“服务更新器”。从下面的源码中可以看到,在ServerListUpdater内部还定义了一个UpdaterAction接口,上面定义的updateAction对象就是以匿名内部类的方式创建了一个它的具体实现,其中doUpdate实现的内容就是对ServerList的具体更新操作。除此之外,“服务更新器”中还定义了一系列控制它和获取它的信息的操作。
public interface ServerListUpdater{ public interface UpdateAction{ void doUpdate(); } //启动服务更新器,传入的UpdateAction对象为更新操作的具体实现 void start(UpdateAction updateAction); //停止服务更新器 String getLastUpdate(); //获取上一次更新到现在的时间间隔,单位为毫秒 long getDurationSinceLastUpdateMs(); //获取错过的更新周期数 int getNumberMissedCycles(); //获取核心线程数 int getCoreThreads(); }
而ServerListUpdater的实现类不多,具体如下图所示。
根据两个类的注释,我们可以很容易地知道它们的作用。
1. PollingServerListUpdater:动态服务列表更新的默认策略,也就是说,DynamicServerListLoadBalancer负载均衡器中的默认实现就是它,它通过定时任务的方式进行服务列表的更新。
2. EurekaNotificationServerListUpdater:该更新器也可服务与Dynamic-ServerListLoadBalancer负载均衡器,但是它的触发机制与PollingServer-ListUpdater不同,它需要利用Eureka的时间监听器来驱动服务列表的更新操作。
下面我们来详细看看它默认实现的PollingServerListUpdater。先从用于启动“服务更新器”的start函数源码看起,具体如下。我们可以看到star函数的实现内容验证了之前提到的:以定时任务的方式进行服务列表的更新。它先创建了一个Runnable的线程实现,在该实现中调用了上面提到的具体更新服务实例列表的方法updateAction.doUpdate(),最后再为这个Runnable线程实现启动了一个定时任务来执行。
@Override public synchronized void start(final UpdateAction updateAction) { if (isActive.compareAndSet(false, true)) { final Runnable wrapperRunnable = new Runnable() { @Override public void run() { if (!isActive . get ()) { if (scheduledFuture != null) { scheduledFuture.cancel(true); } return; } try { updateAction.doUpdate (); lastUpdated = System. currentTimeMillis (); ) catch (Exception e) { logger.warn("Failed one update cycle", e); } } };
scheduledFuture = getRefreshExecutor() .scheduleWithFixedDelay( wrapperRunnable, initialDelayMs, refreshintervalMs, TimeUnit.MILLISECONDS ) ; } else { logger.info("Already active, no-op"); } }
继续看Polling ServerListUpdater的其他内容,我们可以知道用于启动定时任务的两个重要参数initialDelayMs和refreshIntervalMs的默认定义分别为1000和30*1000,单位是毫秒。也就是说,更新服务实例在初始化之后延迟1秒后开始执行,并以30秒为周期重复执行。除了这些内容之外,还能看到它还会记录最后更新时间、是否存活等信息,同时也实现了ServerListUpdater中定义的一些其他操作内容,这些操作相对比较简单,这里不再具体说明,有兴趣的可以自己查看源码了解其实现原理。
ServerListFilter
在了解了更新服务实例的定时任务是如何启动的之后,我们回到updateAction.doUpdate()调用的具体实现位置,在DynamicServerListLoadBalancer中,它的实际实现委托给了updateListOfServers函数,具体实现如下:
public void updateListOfServers() { List<T> servers = new ArrayList<T>(); if (serverListImpl != null) { servers = serverListimpl.getUpdatedListOfServers(); LOGGER.debug("List of Servers for{} obtained from Discovery client: {}", getidentifier(), servers) ;
if (filter ! = null) { servers = filter.getFilteredListOfServers(servers); LOGGER.debug("Filtered List of Servers for{} obtained from Discovery client: {} ",getidentifier(), servers); } } updateAllServerList(servers); }
可以看到,这里终于用到了之前提到的ServerList的getUpdatedListOfServers(),通过之前的介绍已经知道这一步实现了从Eureka Server中获取服务可用实例的列表。在获得了服务实例列表之后,这里又将引入一个新的对象filter,追溯该对象的定义,我们可以找到它是ServerListFilter定义的。
ServerListFilter接口非常简单,该接口中定义了一个方法List getFiltered-ListOfServers(List servers),主要用于实现对服务实例列表的过滤,通过传入的服务实例清单,根据一些规则返回过滤后的服务实例清单。该接口的实现如下图所示:
其中,除了ZonePreferenceServerListFilter的实现是Spring Cloud Ribbon中对Netfilx Ribbon的扩展实现外,其他均是Netflix Ribbon中的原生实现类。下面,我们可以分别看看这些过滤器实现都有什么特点。
1. AbstractServerListFilter:这是一个抽象过滤器,在这里定义了过滤时需要的一个重要依据独享LoadBalancerStats,我们在之前介绍过,该对象存储了关于负载均衡器的一些属性和统计信息等。
public abstract class AbstractServerListFliter<T extends Server> implements ServerListFilter<T> { private volatile LoadBalancerStats stats;
public void setLoadBalancerStats(LoadBalancerStats stats) { this.stats= stats; } public LoadBalancerStats getLoadBalancerStats() { return stats; } }
2. ZoneAffinityServerListFilter:该过滤器基于“区域感知(Zone Affinity)”的方式实现服务实例的过滤,也就是说,它会根据提供服务的实例所处的区域(Zone)与消费者自身的所处区域(Zone)进行比较,过滤掉那些不是同处一个区域的实例。
public List<T> getFilteredListOfServers(List<T> servers) { if (zone != null && (zoneAffinity || zoneExclusive) && servers !=null && servers.size() > 0)( List<T> filteredServers = Lists.newArrayList(Iterables.filter( servers, this.zoneAffinityPredicate.getServerOnlyPredicate())); if (shouldEnableZoneAffinity(filteredServers)) { return filteredServers; ) else if (zoneAffinity) { overrideCounter.increment(); } } return servers; }
从上面的源码中我们可以看到,对于服务实例列表的过滤是通过Iterables.filter(servers,this.zoneAffinityPredicate.getServerOnlyPredicate())来实现的,其中判断依据由ZoneAffinityPredicate实现服务实例与消费者的Zone比较。而在过滤之后,这里并不会马上返回过滤的结果,而是通过shouldEnableZone-Affinity函数来判断是否要启用“区域感知”的功能。从下面shouldEnableZoneAffinity的实现中,我们可以看到,它使用了LoadBalancerStats的getZoneSnapshot(snapshot 快照)方法来获取这些过滤后的同区域实例的基础指标(包含实例数量、断路器断开数、活动请求数、实例平均负载等),根据一系列的算法求出下面的几个评价值并与设置的阈值进行对比(下面的为默认值),若有一个条件符合,就不启用“区域感知”过滤的服务实例清单。这一算法实现为集群出现区域故障时,依然可以依靠其他区域的实例进行正常服务提供了完善的高可用保障。同时,通过这里的介绍,我们也可以关联着来理解之前介绍Eureka的时候提到的对于区域分配设计来保证跨区域故障的高可用问题。
1. blackOutServerPercentage:故障实例百分比(断路器断开数/实例数量)>=0.8。
2. activeReqeustsPerServer:实例平均负载>=0.6。
3. availableServers:可用实例数(实例数量-断路器断开数)<2。
private boolean shouldEnableZoneAffinity(List<T> filtered) { if (!zoneAffinity && !zoneExclusive) { return false; } if (zoneExclusive) { return true; } LoadBalancerStats stats = getLoadBalancerStats(); if (stats == null) { return zoneAffinity; } else { logger. debug ("Determining if zone affinity should be enabled with given server list: {} ", filtered); ZoneSnapshot snapshot = stats.getZoneSnapshot(filtered); double loadPerServer = snapshot.getLoadPerServer(); int instanceCount = snapshot.getinstanceCount(); int circuitBreakerTrippedCount = snapshot.getCircuitTrippedCount(); if (((double) circuitBreakerTrippedCount) / instanceCount >= blackOutServerPercentageThreshold.get() || loadPerServer >= activeReqeustsPerServerThreshold.get() || (instanceCount - circuitBreakerTrippedCount) < availableServersThreshold. get()) { logger.debug("zoneAffinity is overriden. blackOutServerPercentage: {}, activeReqeustsPerServer: {}, availableServers: {}", new Object[] { (double) circuitBreakerTrippedCount / instanceCount, loadPerServer, instanceCount - circuitBreakerTrippedCount}); return false; }else{ return true; } } }
4. DefaultNIWSServerListFilter:该过滤器完全继承自ZoneAffinity-ServerListFilter,是默认的NIWS(Netfilx Internal Web Service)过滤器。
5. ServerListSubsetFilter:该过滤器也继承自ZoneAffinityServer-ListFilter,它非常适用于拥有大规模服务器集群(上百或更多)的系统。因为它可以产生一个“区域感知”结果的子集列表,同时它还能够通过比较服务实例的通信失败数量和并发连接数来判定该服务是否健康来选择性地从服务实例列表中剔除那些相对不够健康的实例。该过滤器的实现主要分为以下三步:
a. 获取“区域感知”的过滤结果,作为候选的服务实例清单。
b. 从当前消费者维护的服务实例子集中剔除那些相对不够健康的实例(同时也将这些实例从候选清单中剔除,防止第三步的时候又被选入),不够健康的标准如下所示。
i. 服务实例的并发连接数超过客户端配置的值,默认为0,配置参数为<clientName>.<nameSpace>.ServerListSubsetFilter.eliminationConnectionThresold.
ii. 服务实例的失败数超过客户端配置的值,默认为0,配置参数为<clientName>.<nameSpace>.ServerListSubsetFilter.eliminationFailureThresold.
iii. 如果按符合上面任一规则的服务实例剔除后,剔除比例小于客户端默认配置的百分比,默认为0.1(10%),配置参数为<clientName>.<nameSpace>.ServerListSubsetFilter.forceEliminatePercent,那么就先对剩下的实例列表进行健康排序,再从最不健康的实例进行剔除,直到达到配置的剔除百分比。
c. 在完成剔除后,清单已经少了至少10%(默认值)的服务实例,最后通过随机的方式从候选清单中选出一批
6. ZonePreferenceServerListFilter:Spring Cloud整合时新增的过滤器。若使用Spring Cloud 整合Eureka 和Ribbon 时会默认使用该过滤器。它实现了通过配置或者Eureka实例元数据的所属区域(Zone)来过滤出同区域的服务实例。如下面的源码所示,它的实现非常简单,首先通过父类ZoneAffinityServerListFilter的过滤器来获取“区域感知”的服务实例列表,然后遍历这个结果,取出根据消费者配置预设的区域Zone来进行过滤,如果过滤的结果是空就直接返回父类获取的结果,如果不为空就返回通过消费者配置的Zone过滤后的结果。
@Override public List<Server> getFilteredListOfServers(List<Server> servers) { List<Server> output = super.getFilteredListOfServers(servers); if (this. zone ! = null && output. size() == servers. size()) { List<Server> local = new ArrayList<Server>(); for (Server server : output){ if (this.zone.equalsignoreCase(server.getZone())) { local.add(server); } } if (!local. isEmpty ()) { return local; } } return output; }
ZoneAwareLoadBalancer
ZoneAwareLoadBalancer负载均衡器是对DynamicServerListLoadBalancer的扩展。在DynamicServerListLoadBalancer中,我们可以看到它并没有重写选择具体服务实例的chooseServer函数,所以它依然会采用在BaseLoadBalancer中实现的算法。使用RoundRobinRule规则(轮询调度),以线性轮询的方式来选择调用的服务实例,该算法实现简单并没有区域(Zone)访问的情况,由于跨区域会产生更高的延迟,这些实例主要以防止区域性故障实现高可用为目的而不能作为常规访问的实例,所以在多区域部署的情况下会有一定的性能问题,而该负载负载均衡器则可以避免这样的问题。那么它是如何实现的呢?
首先,在ZoneAwareLoadBalancer中,我们可以发现,它并没有重写setServersList,说明实现服务实例清单的更新主逻辑没有修改。但是我们可以发现它重写了这个函数setServerListForZones(Map<String,List<Server>>zoneServersMap)。看到这里可能会有一些陌生,因为它并不是接口中定义的必备函数,所以我们不妨去父类DynamicServerListLoadBalancer中寻找以下该函数,我们可以找到下面的定义:
public void setServersList(List lsrv) { super.setServersList(lsrv); List<T> serverList = (List<T>) lsrv; Map<String, List<Server>> serversinZones = new HashMap<String, List<Server>> (); ... setServerListForZones(servers!nZones); }
protected void setServerListForZones(Map<String, List<Server>> zoneServersMap) { LOGGER.debug("Setting server list for zones: {}", zoneServersMap); getLoadBalancerStats().updateZoneServerMapping(zoneServersMap); }
setServerListForZones函数的调用位于更新服务实例清单函数setServers-List的最后,同时从其实现的内容来看,它在父类DynamicServerListLoadBalancer中的作用是根据按区域Zone分组的实例列表,为负载均衡器中的LoadBalancerStats对象创建ZoneStats并放入Map zoneStatsMap集合中,每一个区域Zone对应一个ZoneStats,它用于存储每个Zone的一些状态和统计信息。
在ZoneAwareLoadBalancer中对setServerListForZones的重写如下:
protected void setServerListForZones(Map<String, List<Server>> zoneServersMap) ( super.setServerListForZones(zoneServersMap); if (balancers == null) ( balancers = new ConcurrentHashMap<String, BaseLoadBalancer> (); } //为其设置对应Zone区域的实例清单 for (Map.Entry<String, List<Server>> entry: zoneServersMap.entrySet()) { //讲key大写字符转为小写 String zone = entry.getKey(). toLowerCase(); getLoadBalancer(zone).setServersList(entry.getValue()); } //对Zone区域中实例清单的检查,看看是否有Zone区域下已经没有实例了;是则清空 for (Map. Entry<String, BaseLoadBalancer> existingLBEntry: balancers. entrySet ()) { if (!zoneServersMap.keySet() .contains(existingLBEntry.getKey())) { existingLBEntry.getValue() .setServersList(Collections.emptyList()); } } }
可以看到,在该实现中创建了一个ConcurrentHashMap()类型的balancers对象,它将用来存储每个Zone区域对应的负载均衡器。而具体的负载均衡器的创建则是通过在下面的第一个循环中调用getLoadBalacner函数来完成,同时在创建负载均衡器的时候会创建它的规则(如果当前实现中没有IRule的实例,就创建一个Availability-FilterngRule规则;如果已经有具体实例,就克隆一个)。在创建完负载均衡器后又马上调用setServersList函数为其设置对应Zone区域的实例清单。而第二个循环则是对Zone区域中实例清单的检查,看看是否有Zone区域下已经没有实例了,是的话就将balancers中对应Zone区域的实例列表清空,该操作的作用是为了候选选择节点时,防止过时的Zone区域统计信息干扰具体实例的选择算法。
在了解了该负载均衡器是如何扩展服务实例清单的实现后,我们来具体看看它是如何挑选服务实例,来实现对区域的识别的。
public Server chooseServer(Object key) { if (!ENABLED.get() || getLoadBalancerStats(). getAvailableZones ().size() <= 1) { logger.debug("Zone aware logic disabled or there is only one zone"); return super.chooseServer(key); } Server server = null; try { LoadBalancerStats lbStats = getLoadBalancerStats(); Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats); logger.debug("Zone snapshots: {} ", zoneSnapshot);
... Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(),
triggeringBlackoutPercentage.get()); logger.debug("Available zones: {}", availableZones); if (availableZones ! = null && available Zones. size () < zoneSnapshot. keySet () . size ()) { String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot,availableZones); logger. debug ("Zone chosen: {} ", zone) ; if (zone ! = null) { BaseLoadBalancer zoneLoac!Balancer = getLoadBalancer(zone); server = zoneLoac!Balancer.chooseServer(key);
}
}
}catch (Throwable e){
logger.error("Unexpected exception when choosing server using zone aware logic", e);
}
if (server 1 = null) {
return server;
} else {
logger.debug("Zone avoidance logic is not invoked.");
return super.chooseServer(key);
}
}
从源码中我们可以看到,只有当负载均衡器中维护的实例所属的Zone区域的个数大于1的时候才会执行这里的选择策略,否则还是将使用父类的实现。当Zone区域的个数大于1的时候,它的实现步骤如下所示。
1. 调用ZoneAvoidanceRule中的静态方法createSnapshot(lbstats),为当前负载均衡器中所有的Zone区域分别创建快照,保存在Map zoneSnapshot中,这些快照中的数据将用于后续的算法。
2. 调用ZoneAvoidanceRule中的静态方法getAvailableZones(zoneSnapshot,triggeringLoad.get(),triggeringBlackoutPercentage.get()),来获取可用的Zone区域集合,在该函数中会通过Zone区域快照中的统计数据来实现可用区的挑选。
a. 首先它会提出符合这些规则的Zone区域:所属实例数为零的Zone区域;Zone区域内实例的平均负载小于零,或者实例故障率(断路器断开次数/实例数)大于等于阈值(默认为0.99999)。
b. 然后根据Zone区域的实例平均负载计算出最差的Zone区域,这里的最差指的是实例平均负载最高的Zone区域。
c. 如果在上面的过程中没有符合提出要求的区域,同时实例最大平均负载小于阈值(默认为20%),就直接返回所有Zone区域为可用区域。否则,从最坏Zone区域集合中随机选择一个,将它从可用Zone区域集合中剔除。
3. 当获得的可用Zone区域集合不为空,并且个数小于Zone区域总数,就随机选择一个Zone区域。
4. 在确定了某个Zone区域后,则获取了对应Zone区域的服务均衡器,并调用chooseServer来选择具体的服务实例,而在chooseServer中将使用IRule接口中的choose函数来选择具体的服务实例。在这里,IRule接口的实现会使用ZoneAvoidanceRule来挑选出具体的服务实例。
关于ZoneAvoidanceRule的策略以及其他一些还未提到的负载均衡策略,我们将在下一节做更加详细的解读。
负载均衡策略
通过上面的源码解读,我们已经对Ribbon实现的负载均衡器以及其中包含的服务实例过滤器、服务实例信息的存储对象、区域的信息快照等都有了深入的认识和理解,但是对于负载均衡器中的服务实例选择策略只是讲解了几个默认实现的内容,而对于IRule的其他实现还没有详细解读,下面我们来看看在Ribbon中都提供了那些负载均衡的策略实现。
如下图所示,可以看到在Ribbon中实现了非常多的选择策略,其中也包含了我们在前面内容中提到过的RoundRobinRule和ZoneAvoidanceRule。下面我们来详细解读一下IRule接口的各个实现。
AbstractLoadBalancerRule
负载均衡策略的抽象类,在该抽象类中定义了负载均衡器ILoadBalancer对象,该对象能够在具体实现选择服务策略时,获取到一些负载均衡器中维护的信息来作为分配依据,并以此设计一些算法来实现针对特定场景的高效策略。
public abstract class AbstractLoadBalancerRule implements IRule, IClientConfigAware { private ILoadBalancer lb;
@Override public void setLoadBalancer(ILoadBalancer lb) { this.lb= lb; }
@Overr ide public ILoadBalancer getLoadBalancer() { return lb; } }
RandomRule
该策略实现了从服务实例清单中随机选择一个服务实例的功能。它的具体实现如下,可以看到IRule接口的choose(Object key)函数实现,委托给了该类中的choose(ILoadBalancer lb,Object key),该方法增加了一个负载均衡器对象的参数。从具体的实现上来看,它会使用传入的负载均衡器来获得可用实例列表upList和所有实例列表allList,并通过rand.nextInt(serverCount)函数来获取一个随机数,并将该随机数作为upList的索引值来返回具体实例,正常情况下每次选择都应该先出一个服务实例,如果出现死循环获取不到服务实例时,则很可能存在并发的bug。
@Override public Server choose(Object key) { return choose(getLoadBalancer(), key); }
public Server choose(ILoadBalancer lb, Object key) { ... Server server = null; while (server == null) { if (Thread.interrupted()) { return null; } List<Server> upList = lb.getReachableServers(); List<Server> allList = lb.getA11Servers(); int serverCount = allList.size(); if (serverCount == 0) { return null; } int index = rand.nextint(serverCount); server = upList.get(index); if (server == null) { Thread.yield(); continue; } if (server.isAlive()) { return (server); } server = null; Thread. yield() ; } return server; }
RoundRobinRule
该策略实现了按照线性轮询的方式一次选择每个服务实例的功能。它的具体实现如下,其详细结构与RandomRule非常类似。除了循环条件不同外,就是从可用列表中获取所谓的逻辑不同。从循环条件中,我们可以看到增加了一个count计数变量,该变量会在每次循环之后累加,也就是说,如果一直选择不到server超过10次,那么就会结束尝试,并打印一个警告信息:No available alive servers after 10 tries from load balancer:... 而线性轮询的实现则是通过AtomicInteger nextServerCyclicCounter对象实现,每次进行实例选择时通过调用incrementAndGetModulo函数实现递增。
public Server choose(ILoadBalancer lb, Objectkey) { ... Server server = null; int count = 0; while (server == null && count++ < 10) { List<Server> reachableServers = lb.getReachableServers(); List<Server> al1Servers = lb. getAllServers () ; int upCount = reachableServers.size(); int serverCount = al1Servers.size(); if ((upCount == 0) I I (serverCount == 0)) { log.warn("No up servers available from load balancer: "+ lb); return null; } int nextServerindex = incrementAndGetModulo(serverCount); server = allServers.get(nextServerindex); if (server == null) { Thread. yield(); continue; } if (server.isAlive() && (server.isReadyToServe())) { return (server); } server = null; } if (count >= 10) { log.warn("No available alive servers after 10 tries from load balancer: " + lb); } return server; }
RetryRule
该策略实现了一个具体重试机制的实例选择功能。从下面的实现中我们可以看到,在其内部还定义了一个IRule对象,默认使用了RoundRobinRule实例。而在choose方法中则实现了对内部定义的策略进行反复尝试的策略,若期间能够选择到具体的服务实例就返回,若选择不到就根据设置的尝试结束时间为阈值(maxRetryMillis参数定义的值+choose方法开始执行的时间戳),当超过该阈值后就返回null。
public class RetryRule extends AbstractLoadBalancerRule { IRule subRule = new RoundRobinRule(); long maxRetryMillis = 500; ... public Server choose(ILoadBalancer lb, Object key) { long requestTime = System. currentTimeMillis (); long deadline = requestTime + maxRetryMillis; Server answer = null; answer = subRule.choose(key); if (((answer == null) || (! answer. isAlive ())) && (System.currentTimeMillis() < deadline)) { InterruptTask task = new InterruptTask(deadline- System. currentTimeMillis ()); while (! Thread. interrupted ()) { answer = subRule.choose(key); if (((answer == null) || (!answer. isAlive ())) && (System.currentTimeMillis() < deadline)) { //使当前线程由执行状态,变成为就绪状态,让出cpu时间,在下一个线程执行时候,此线程有可能被执行,也有可能没有被执行。 Thread.yield(); } else { break; } } task.cancel(); } if ((answer == null) || (!answer. isAlive ())) { return null; } else { return answer; } } ... }
WeightedResponseTimeRule
该策略是对RoundRobinRule的扩展,增加了根据实例的运行情况来计算权重,并根据权重来挑选实例,以达到更优的分配效果,它的实现主要有三个核心内容。
定时任务
WeightedResponseTimeRule策略在初始化的时候会通过serverWeightTimer.schedule(new DynamicServerWeightTash(),0,serverWeightTaskTimerInterval)启动一个定时任务,用来为每个服务实例计算权重,该任务默认30秒执行一次。
class DynamicServerWeightTask extends TimerTask{ public void run(){ ServerWeight serverWeight = new ServerWeight(); try { serverWeight.maintainWeights(); }catch (Throwable t){ logger.error("Throwable caught while running DynamicServerWeightTask for" +name,t); } } }
权重计算
在源码中我们可以轻松找到用于存储权重的对象List <Double> accumulated-Weights = new ArrayList<Double>(),该List中每个权重值所处的位置对应了负载均衡器维护的服务实例清单中所有实例在清单中的位置。
维护实例权重的计算过程通过maintainWeights函数实现,具体如下面的代码所示:
public void maintainWeights() { ILoadBalancer lb = getLoadBalancer(); ... try { logger.info("Weight adjusting job started"); AbstractLoadBalancer nlb = (AbstractLoadBalancer) lb; LoadBalancerStats stats = nlb.getLoadBalancerStats(); ... //计算所有实例的平均响应时间的总和: totalResponseTime double totalResponseTime = 0; for (Server server : nlb.getAllServers ()) { //如果服务实例的状态快照不在缓存中, 那么这里会进行自动加载 ServerStats ss = stats.getSingleServerStat(server); totalResponseTime += ss.getResponseTimeAvg(); } //逐个计算每个实例的权重: weightSoFar + totalResponseTime -实例的平均响应时间 Double weightSoFar = 0.0; List<Double> finalWeights = new ArrayList<Double>(); for (Server server : nlb. getAllServers ()) { ServerStats ss = stats.getSingleServerStat(server); double weight = totalResponseTime - ss.getResponseTimeAvg(); weightSoFar += weight; finalWeights.add(weightSoFar); } setWeights(finalWeights); } catch (Throwable t){ logger.error("Exception while dynamically calculating server weights", t); } finally { serverWeightAssignmentinProgress.set(false); } }
该函数的实现主要分为两个步骤:
1. 根据LoadBalancerStats中记录的每个实例的统计信息,累加所有实例的平均响应时间,得到总平均响应时间totalResponseTime,该值会用于后续的计算。
2. 为负载均衡器中维护的实例清单逐个计算权重(从第一个开始),计算规则为weightSoFar + totalResponseTime - 实例的平均响应时间,其中weightSoFar初始化为零,并且每计算好一个权重需要累加到weightSoFar上供下一次计算使用。
举个简单的例子来理解这个计算过程,假设有4个实例A、B、C、D,它们的平均响应时间为10、40、80、100,所以总响应时间是10+40+80+100=230,每个实例的权重为总响应时间与实例自身的平均响应时间的差的累积所得,所以实例A、B、C、D的权重分别如下所示。
1. 实例A:230-10=220
2. 实例B:220+(230-40)=410
3. 实例C:410+(230-80)=560
4. 实例D:560+(230-100)=690
需要注意的是,这里的权重值只是表示了各实例权重区间的上限,并非某个实例的优先级,所以不是数值越大被选中的概率就越大。那么什么是权重区间呢?以上面例子的设计结果为例,它实际上是为了这4个实例构建了4个不同的区间,每个实例的区间下限是上一个实例的区间上限,而每个实例的区间上限则是我们上面计算并存储于ListacumulatedWeights中的权重值,其中第一个实例的下限默认为零。所以,根据上面示例的权重计算结果,我们可以得到每个实例的权重区间。
1. 实例A:【0,220】
2. 实例B:【220,410】
3. 实例C:【410,560】
4. 实例D:【560,690】
不难发现,实际上每个区间的宽度就是:总的平均响应时间 - 实例的平均响应时间,所以实例的平均响应时间越短、权重区间的宽度越大,而权重区间的宽度越大被选中的概率就越高。可能很多读者会问,这些区间边界的开闭是如何确定的呢?为什么不那么规则?下面我们会通过实例选择算法的解读来解释。
实例选择
WeightedResponseTimeRule选择实例的实现与之前介绍的算法结构类似,下面是它主体的算法(省略了循环体和一些判断等处理)
public Server choose(ILoadBalancer lb, Object key) { ... List<Double> currentWeights = accumulatedWeights; ... List<Server> allList = lb. getA11Servers (); int serverCount = allList. size(); if (serverCount == 0) { return null; } int serverindex = 0; //获取最后一个实例的权重 double maxTotalWeight = currentWeights. size () == 0 ? 0 : currentWeights. get(currentWeights.size() - l); if (maxTotalWeight < 0.001d) { //如果最后一个实例的权重值小于0.001, 则采用父类实现的线性轮询的策略 server = super. choose (getLoadBalancer(), key); if(server == null) { return server; } } else { //如果最后一个实例的权重值大于等于0.001, 就产生一个(0, maxTotalWeight)的随机数 double randomWeight = random.nextDouble () * maxTotalWeight; int n = 0; for (Double d: currentWeights) { //遍历维护的权重清单, 若权重大于等于随机得到的数值, 就选择这个实例 if (d >= randomWeight) { serverindex = n; break; } else { n++; } } server = allList.get(serverindex); } ... return server; }
从源码中我们可以看到,选择实例的核心过程就两步:
1. 生成一个[ 0, 最大权重值 ] 区间内的随机数。
2. 遍历权重列表,比较权重值与随机数的大小,如果权重值大于等于随机数,就拿当前权重列表的索引值去服务实例列表中获取具体的实例。这就是在上一节中提到的服务实例会根据权重区间挑选的原理,而权重区间边界的开闭原则根据算法,正常每个区间为( x,y ] 的形式,但是第一个实例和最后一个实例为什么不同呢?由于随机数的最下取值可以为0,所以第一个实例的下限是闭区间,同时随机数的最大值取不到最大权重值,所以最后一个实例的上限是开区间。
若继续以上面的数据为例进行服务实例的选择,则该方法会从 [ 0 , 690 ) 区间中选出一个随机数,比如选出的随机数为230,由于该值位于第二个区间,所以此时就会选择实例B来进行请求。
ClientConfigEnableRoundRobinRule(轮询调度)
该策略较为特殊,我们一般不直接使用它。因为它本身并没有实现什么特殊的处理逻辑,正如下面的源码所示,在它的内部定义了一个RoundRobinRule策略,而choose函数的实现也正是使用了RoundRobinRule的线性轮询机制,所以它实现的功能实际上与RoundRobinRule相同,那么定义它有什么特殊的用处呢?
虽然我们不会直接使用该策略,但是通过继承该策略,默认的choose就实现了线性轮询机制,在子类中做一些高级策略时通常有可能会存在一些无法实施的情况,那么就可以用父类的实现作为备选。在后文中我们将继续介绍的高级策略均是基于ClientConfigEnabledRoundRobinRule的扩展。
public class ClientConfigEnabledRoundRobinRule extends AbstractLoadBalancerRule {
RoundRobinRule roundRobinRule = new RoundRobinRule(); ... @Override public Server choose(Object key) { if (roundRobinRule != null) { return roundRobinRule.choose(key); } else { throw new IllegalArgumentException( "This class has not been initialized with the RoundRobinRule class") ; } } }
BestAvailableRule
该策略继承自ClientConfigEnabledRoundRobinRule,在实现中它注入了负载均衡器的统计对象LoadBalancerStats保存的实例统计信息来选择满足要求的实例。从如下源码中我们可以看到,它通过遍历负载均衡器中维护的所有服务实例,会过滤掉故障的实例,并找出并发请求数最小的一个,所以该策略的特性是可选出最空闲的实例。
public Server choose(Object key) { if (loadBalancerStats == null) { return super.choose(key); } List<Server> serverList = getLoadBalancer().getAllServers(); int minimalConcurrentConnections = Integer.MAX—VALUE; long currentTime = System.currentTimeMillis(); Server chosen = null; for (Server server: serverList) { ServerStats serverStats = loadBalancerStats.getSingleServerStat(server); if (!serverStats.isCircuitBreakerTripped(currentTime)) { int concurrentConnections = serverStats. getActiveRequestsCount (currentTime); if (concurrentConnections < minimalConcurrentConnections) { minimalConcurrentConnections = concurrentConnections; chosen = server; } } } if (chosen == null) { return super.choose(key); } else { return chosen; } }
同时,由于该算法的核心依据是统计对象是loadBalancerStats,当其为空的时候,该策略是无法执行的。所以从源码中我们可以看到,当loadBalancerStats为空的时候,它会采用父类的线性轮询策略,正如我们在介绍ClientConfigEnabledRoundRobinRule,所以它们都会具有这样的特性。
PredicateBasedRule
这是一个抽象策略,它也继承了ClientConfigEnabledRoundRobinRule,从其命名中可以猜出这是一个基于Predicate实现的策略,Predicate是Google GuavaCollection 工具对集合进行过滤的条件接口。
如下面的源码所示,它定义了一个抽象函数getPredicate来获取AbstractServer-Predivate对象的实现,而在choose函数中,通过AbstractServerPredicate的chooseRoundRobinAfterFiltering函数来选出具体的服务实例。从该函数的命名我们也大致能猜出它的基础逻辑:先通过子类中实现的Predicate逻辑来过滤一部分服务实例,然后再以线性轮询的方式从过滤后的实例清单中选出一个。
public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule { public abstract AbstractServerPredicate getPredicate () ;
@Override public Server choose(Object key) { ILoadBalancer lb= getLoadBalancer(); Optional<Server> server= getPredicate().chooseRound.RobinAfterFiltering(lb.getA11Servers (), key); if ( server.isPresent()){ return server.get(); } else { return null; } } }
通过下面AbstractServerPredicate的源码片段,可以证实我们上面所做的猜测。在上面choose函数中调用的chooseRoundRobinAfterFiltering方法先通过内部定义的getEligibleServers函数来获取备选的实例清单(实现了过滤),如果返回的清单为空,则用Optional.absent()来表示不存在,反之则以线性轮询的方式从备选清单中获取一个实例。
public abstract class AbstractServerPredicate implements Predicate<PredicateKey> { ... public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) { List<Server> eligible= getEligibleServers(servers, loadBalancerKey); if (eligible.size() == 0) { return Optional.absent(); } return Optional.of (eligible.get(nextindex. getAndincrement () % eligible.size())) ; } public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) { if (loadBalancerKey == null) { return ImmutableList.copyOf(Iterables.filter(servers, this.getServerOnlyPredicate())); } else { List results = Lists.newArrayList(); for (Server server: servers) { if (this.apply(new PredicateKey(loadBalancerKey, server))) { results.add(server); } } return results; } } }
在了解了整体逻辑之后,我们来详细看看实现过滤功能的getEligibleServers函数。从源码上看,它的实现结构简单清晰,通过遍历服务清单,使用this.apply方法来判断实例是否需要保留,如果是就添加到结果列表中。
可能到这里,不熟悉Google Guava Collections集合工具的读者会感到困惑,这个apply在AbstractServerpredicate中找不到它的定义,那么它是如何实现过滤的呢?实际上,AbstractServetPredicate实现了com.google.common.base.Predicate接口,而apply方法是该接口中的定义,主要用来实现过滤条件的判断逻辑,它输入的参数则是过滤条件需要用到一些信息(比如源码中的new PredicateKey(loadBalancerKey , server)),它传入了关于实例的统计信息和负载均衡器的选择算法传递过来的key)。既然在AbstractServerPredicate中我们未能找到apply的实现,所以这里的chooseRoundRobinAfterFiltering函数定义了一个模板策略:“先过滤清单,再轮询选择”。对于如何过滤,需要我们在AbstractServerPredicate的子类中实现apply方法来确定具体的过滤策略。
后面我们将要介绍的两个策略就是基于此抽象策略实现,只是它们使用了不同的Predicate实现来完成过滤逻辑以达到不同的实例选择效果。
Google Guava Collections 是一个对java Collections Framework增强和扩展的开源项目。虽然Java Collections Framework已经能够满足我们大多数情况下使用集合的要求,但是当遇到一些特殊的情况时我们的代码会比较冗长且容易出错。Guava Collections可以帮助我们让集合操作代码更为简短精练并大大增强代码的可读性。
AvailabilityFilteringRule
该策略继承自上面介绍的抽象策略PredicateBaseRule,所以它也继承了“先过滤清单,再轮询选择”的基本处理逻辑,其中过滤条件使用了AvailabilityPredicate:
public class AvailabilityPredicate extends AbstractServerPredicate {
... public boolean apply(@Nullable PredicateKey input) { LoadBalancerStats stats = getLBStats() ; if (stats == null) { return true; } return !shouldSkipServer(stats.getSingleServerStat(input.getServer())); }
private boolean shouldSkipServer(ServerStats stats) { if ((CIRCUIT_BREA.KER_FILTERING.get() && stats.isCircuitBreakerTripped()) || stats.getActiveRequestsCount () >= activeConnectionsLimit.get ()) { return true; } return false; } }
从上述源码中,我们可以知道它的主要过滤逻辑位于shouldSkipServer方法中,它主要判断服务实例的两项内容:
1. 是否故障,即断路器是否生效已断开。
2. 实例的变法请求数大于阈值,默认为2^32-1,,该配置可通过参数<clientName>.<nameSpace>.ActiveConnectionsLimit来修改。
这两项内容中只要有一个满足apply就返回false(代表该节点可能存在故障或负载过高),都不满足就返回true。
在该策略中,除了实现了上面的过滤方法之外,对于choose的策略也做了一些改进优化,所以父类的实现对于它来说只是一个备用选项,其具体实现如下所示:
public Server choose(Object key){ int count = 0; Server server = roundRobinRule.choose(key); while(count++<=10){ if(predicate.apply(new PredicateKey(server))){ return server; } server = roundRobinRule.choose(key); } return super.choose(key); }
可以看到,它并没有像在父类中那样,先遍历所有的节点进行过滤,然后在过滤后的集合中选择实例。而是先以线性的方式选择一个实例,接着用过滤条件来判断该实例是否满足要求,若满足就直接使用该实例,若不满足要求就再选择下一个实例,并检查是否满足要求,如此循环进行,当这个过程重复了10次还是没有找到符合要求的实例,就采用父类的实现方案。
简单地说,该策略通过线性抽样的方式直接尝试寻找可用且较空闲的实例来使用,优化了父类每次都要遍历其所有实例的开销。
ZoneAvoidanceRule
该策略我们在介绍负载均衡器ZoneAwareLoadBalancer时已经提到过,它也是PredicateBaseRule的具体实现类。在之前的介绍中主要针对ZoneAvoidanceRule中用于选择Zone区域策略的一些静态函数,比如createSnapShot、getAvailableZones。在这里我们将详细看看ZoneAvoidanceRule作为服务实例过滤条件的实现原理。从下面ZoneAvoidanceRule的源码片段中可以看到,它使用了CompositePredicate来进行服务实例清单的过滤。这是一个组合过滤条件,在其构造函数中,它以ZoneAvoidancePredicate为主过滤条件,AvailabilityPredicate为次过滤条件初始化了组合过滤条件的实例。
public class ZoneAvoidanceRule extends PredicateBasedRule { ... private CompositePredicate compositePredicate;
public ZoneAvoidanceRule() { super(); ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this); AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this); compositePredicate = createCompositePredicate(zonePredicate, availabilityPredicate); } ... }
ZoneAvoidanceRule在实现的时候并没有像AvailabilityFilteringRule那样重写choose函数来优化,所以它完全遵循了父类的过滤主逻辑:“先过滤清单,再轮询选择”。其中过滤清单的条件就是我们上面提到的以ZoneAvoidancePredicate为主过滤条件、AvailabilityPredicate为次过滤条件的组合过滤条件Composite-Predicate。从CompositePredicate的源码片段中,我们可以看到它定义了一个主过滤条件AbstractServerPredicate delegate以及一组次过滤条件列表List fallbacks,所以它的次过滤列表是可以拥有多个的,并且由于它采用了List存储所以次过滤条件是按顺序执行的。
public class CompositePredicate extends AbstractServerPredicate { private AbstractServerPredicate delegate; private List<AbstractServerPredicate> fallbacks = Lists.newArrayList();
private int minimalFilteredServers = 1; private float minimalFilteredPercentage = 0;
@Override public List<Server> getEligibleServers(List<Server> servers, ObjectloadBalancerKey) { List<Server> result = super.getEligibleServers(servers, loadBalancerKey); Iterator<AbstractServerPredicate> i = fallbacks.iterator(); while (! (result.size() >= minimalFilteredServers && result.size() > (int)(servers. size() * minimalFilteredPercentage)) && i. hasNext ()) { AbstractServerPredicate predicate = i. next() ; result = predicate.getEligibleServers(servers, loadBalancerKey); return result; }
}
在获取过滤结果的实现函数getEligibleServers中,它的处理逻辑如下所示。
1. 使用主过滤条件对所有实例过滤并返回过滤后的实例清单。
2. 依次使用次过滤条件列表中的过滤条件对主过滤条件的结果进行过滤。
3. 每次过滤之后(包括主过滤条件和次过滤条件),都需要判断下面两个条件,只要有一个符合就不再进行过滤,将当前结果返回供线性轮询算法选择:
a. 过滤后的实例总数 >= 最小过滤实例数(minimalFilteredServers,默认为1)。
b. 过滤后的实例比例 > 最小过滤百分比(minimalFilteredPercentage,默认为0)。
配置详解
上一节是对Spring Cloud Ribbon的源码分析,主要对几个重要接口的讲解。下面,将详细介绍Ribbon在使用时的各种配置方式。
自动化配置
由于Ribbon中定义的每一个接口都有多种不同的策略实现,同时这些接口之间又有一定的依赖关系,这使得第一次使用Ribbon的开发者很难上手,不知道如何选择具体的实现策略以及如何组织它们的关系。Spring Cloud Ribbon中的自动化配置洽洽能够解决这样的痛点,在引入Spring Cloud Ribbon的依赖之后,就能够自动化构建下面这些接口的实现。
1. IClientConfig:Ribbon的客户端配置,默认采用com.netflix.client.config.DefaultClientConfigImpl实现。
2. IRule:Ribbon的负载均衡策略,默认采用com.netflix.loadbalancer.ZoneAvoidanceRule实现,该策略能够在多区域环境下选出最佳区域的实例进行访问。
3. IPing:Ribbon的实例检查策略,默认采用com.netflix.ladbalancer.NoOpPing实现,该检查策略是一个特殊的实现,实际上它并不会检查实例是否可用,而是始终返回true,默认认为所有服务实例都是可用的。
4. ServerList<Server>:服务实例清单的维护机制,默认采用com.netflix.loadbalaner.ConfigurationBaseServerList实现。
5. ServerListFilter<Server>:服务实例清单过滤机制,默认采用org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter实现,该策略能够优先过滤出与请求调用方处于同区域的服务实例。
6. ILoadBalancer:负载均衡器,默认采用com.netflix.loadbalancer.ZoneAwareLoadBalancer实现,它具备区域感知的能力。
上面这些自动化配置内容仅在没有引入Spring Cloud Eureka等服务治理框架时如此,在同时引入Eureka 和 Ribbon依赖时,自动化配置会有一些不同,后续我们会做详细的介绍。
通过自动化配置的实现,我们可以轻松地实现客户端负载均衡。同时,针对一些个性化需求,我们也可以方便地替换上面的这些默认实现。只需在Spring Boot应用中创建对应的实现实例就能覆盖这些默认的配置实现。比如下面的配置内容,由于创建了PingUrl实例,所以默认的NoOpPing就不会被创建。
@Configuration public class MyRibbonConfiguration { @Bean public IPing ribbonPing(IClientConfig config) { return new PingUrl(); } }
另外,也可以通过使用@RibbonClient注解来实现更细粒度的客户端配置,比如下面的代码实现了为hello-service服务使用HelloServiceConfiguration中的配置。
@Configuration @RibbonClient(name = "hello-service", configuration = HelloServiceConfiguration. class) public class RibbonConfiguration { }
Camden 版本对RibbonClient配置的优化
上面我们介绍了在Brixton版本中对RibbonClient的IPing、IRule等接口实现进行个性化定制的方法,主要通过独立创建一个Configuration类来定义IPing、IRule等接口的具体实现Bean,然后在创建RibbonClient时指定要使用的具体Configuration类来覆盖自动化配置的默认实现。虽然这种方式已经能够实现个性化的定义,但是当有大量这类配置的时候,对于各个RibbonClient的指定配置信息都将分散在这些配置类的注解定义中,这使得管理和修改都变得非常不方便。所以,在Camden版本中,Spring Cloud Ribbon对RibbonClient定义个性化配置的方法做了进一步优化。可以直接通过<clientname>.ribbon.<key>=<value>的形式进行配置。比如我们要实现与上面例子一样的配置(将hello-service服务客户端的IPing接口实现替换为PingUrl),只需在application.properties配置中增加下面的内容即可:
hello-service.ribbon.NFLoadBalancerPingClassName = com.netflix.loadbalancer.PingUrl
其中hello-service为服务名,NFLoadBalancerPingClassName参数用来指定具体的IPing接口实现类。在Camden版本中,Spring Cloud Ribbon新增了一个org.springframework.cloud.netflix.ribbon.PropertiesFactory类来动态地为RibbonClient创建这些接口实现。
public class PropertiesFactory (
@Autowired private Environment environment; private Map<Class, String> classToProperty = new HashMap<>();
public PropertiesFactory() { classToProperty.put(ILoadBalancer.class, "NFLoadBalancerClassName"); classToProperty.put(IPing.class, "NFLoadBalancerPingClassName"); classToProperty.put(IRule.class, "NFLoadBalancerRuleClassName"); classToProperty.put(ServerList.class, "NIWSServerListClassName"); classToProperty.put(ServerListFilter.class, "NIWSServerListFilterClassName"); } public boolean isSet(Class clazz, String name) { return StringUtils.hasText(getClassName(clazz, name)); }
public String getClassName(Class clazz, String name) { if (this.classToProperty.containsKey(clazz)) { String classNameProperty = this.classToProperty.get(clazz); String className = environment.getProperty(name + "." + NAMESPACE + "." + classNameProperty); return className; } return null; } @SuppressWarnings ("unchecked") public <C> C get(Class<C> clazz, IClientConfig config, String name) { String className = getClassName(clazz, name); if (StringUtils.hasText(className)) { try { Class<?> tolnstantiate = Class.forName(className); return (C) instantiateWithConfig(toinstantiate, config); } catch (ClassNotFoundException e) { throw new IllegalArgumentException("Unknown class to load "+classNarne+" for class "+ clazz +" named"+ name); } } return null; } }
从上述源码定义中可以看到,除了NFLoadBalancerPingClassName参数之外,还提供了其他几个接口的动态配置实现,具体如下所述。
1. NFLoadBalancerClassName:配置ILoadBalancer接口的实现。
2. NFLoadBalancerPingClassName:配置IPing接口的实现。
3. NFLoadBalancerRuleClassName:配置IRule接口的实现。
4. NIWSServerListClassName:配置ServerList接口的实现。
5. NIWSServerListFilterClassName:配置ServerListFilter接口的实现。
所以,在Camden版本中我们可以通过配置的方式,更加方便地为RibbonCliet指定ILoadBalancer、IPing、IRule、ServerList和ServerListFilter的定制化实现。
参数配置
对于Ribbon的参数配置通常有两种方式:全局配置以及指定客户端配置。
1. 全局配置的方式很简单,只需要使用ribbon.<key> = <value>格式进行配置即可。其中,<key>代表了Ribbon客户端配置的参数名,<value>则代表了对应参数的值。比如,我们可以像下面这样全局配置Ribbon创建连接的超时时间:
ribbon.ConnectTimeout = 250
全局配置可以作为默认值进行设置,当指定客户端配置了响应key的值时,将覆盖全局配置的内容。
2. 指定客户端的配置方式采用<client>.ribbon.<key>=<value>的格式进行配置。其中,<key>和<value>的含义同全局配置相同,而<client>代表了客户端的名称,如上文我们在@RibbonClient中指定的名称,也可以将它理解为是一个服务名。为了方便理解这种配置方式,我们举一个具体的例子:假设,有一个服务消费者通过RestTemPlate访问hello-service服务的/hello接口,这时我们会这样调用restTemplate.getForEntity("http://hello-service/hello",String.class).getBody(); 。如果没有服务治理框架的帮助,我们需要为该客户端指定具体的实例清单,可以指定服务名来做详细的配置,具体如下:
hello-service.ribbon.listOfServers = localhost:8001,localhost:8002,localhost:8003
对于Ribbon参数的key以及value类型的定义,可以通过查看com.netflix.client.config.CommonClientfigKey类获得更为详细的配置内容(这里不详细介绍)
与Eureka结合
当在Spring Cloud的应用中同时引入Sping Cloud Ribbon和Spring Cloud Eureka依赖时,会触发Eureka中实现的对Ribbon的自动化配置。这时ServerList的维护机制实现将被com.netflix.niws.loadbalancer.DiscoveryEnableNIWSServerList的实例所覆盖,该实现会将服务清单列表交给Eureka的服务治理机制来维护:IPing 的实现将为com.netflix.ribbon.eureka.DomainExtratingServerList,其目的是为了让实例维护策略更加通用,所以将使用物理元数据来进行负载均衡,而不是使用原生的AWS AMI元数据。
在与Spring Cloud Eureka 结合使用的时候,我们的配置将会变得更加简单。不再需要通过类似hello-service.ribbon.listOfServers的参数来指定具体的服务实例清单,因为Eureka将会为我们维护所有服务的实例清单,而指定客户端的配置方式可以直接使用Eureka中的服务名作为<client>来完成针对各个微服务的个性化配置。
此外,由于Spring Cloud Ribbon默认实现了区域亲和策略,所以,我们可以通过Eureka实例的元数据配置来实现区域化的实例配置方案。比如,可以将处于不同机房的实例配置成不同的区域值,以作为跨区域的容错机制实现。而实现的方式非常简单,只需在服务实例的元数据中增加zone参数来指定自己所在的区域,比如:
eureka.instance.metadataMap.zone=shanghai
在Spring Cloud Ribbon 与Spring Cloud Eureka 结合的工程中,我们也可以通过参数配置的方式来禁用Eureka 对Ribbon 服务实例的维护实现。只需在配置文件中加入如下参数,这时我们对于服务实例的维护就又将回归到使用<client>.ribbon.listOfServers参数配置的方式来实现了。
ribbon.eureka.enabled = false
ribbon.eureka.enable=false
由于Spring Cloud Eureka 实现的服务治理机制强调了CAP原理(一致性,可用性,可靠性)中的AP,即可用性与可靠性,它与ZooKeeper这类强调CP(一致性、可靠性)的服务治理框架最大的区别就是,Eureka为了实现更高的服务可用性,牺牲了一定的一致性,在极端情况下它宁愿接收故障实例也不要丢掉“健康”实例,比如,当服务注册中心的网络发生故障断开时,由于所有的服务实例无法维持续约心跳,在强调AP的服务治理中将会把所有服务实例都剔除掉,而Eureka则会因为超过85%的实例丢失心跳而会触发保护机制,注册中心将会保留此时的所有节点,以实现服务间依然可以进行互相调用的场景,即时其中有部分故障节点,但这样做可以继续保证大多数的服务正常消费。
由于Spring Cloud Eureka在可用性与一致性上的取舍,不论是由于触发了保护机制还是服务剔除的延迟,引起服务调用到故障实例的时候,我们还是希望能够增强对这类问题的容错。所以,我们在实现服务调用的时候通常会加入一些重试机制。在目前我们使用的Brixton版本中,对于重试机制的实现需要我们自己来扩展完成。而从Camden SR2版本开始,Spring Cloud整合了Spring Retry来增强RestTemplate实现的服务访问就会自动根据配置来实现重试策略。
以我们之前对hello-service服务的调用为例,可以在配置文件中增加如下内容:
spring.cloud.loadbalancer.retry.enabled= true hystrix.comand.default.execution.isolation.thread.timeoutInMilliseconds=10000 hello-service.ribbon.ConnectTimeout=250 hello-service.ribbon.ReadTimeout=1000 hello-service.ribbon.OkToRetryOnAllOperations=true hello-service.ribbon.MaxAutoRetriesNextServer=2 hello-service.ribbon.MaxAutoRetries=1
其中各参数的配置说明如下所示。
1. spring.cloud.loadbalancer.retry.enabled:该参数用来开启重试机制,它默认是关闭的。这里需要注意,官方文档中的配置参数少了enabled。该参数的源码定义如下:
@ConfigurationProperties("spring.cloud.loadbalancer.retry") public class LoadBalancerRetryProperties{ private boolean enabled = false; ... }
2. hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds:断路器的超时时间需要大于Ribbon的超时时间,不然不会触发重试。
3. hello--service.ribbon.ConnectTimeout:请求连接的超时时间。
4. hello-service.ribbon.ReadTimeout:请求处理的超时时间。
5. hello-service.ribbon.OkToRetryOnAllOperations:对所有操作请求都进行重试。
6. hello-service.ribbon.MaxAutoRetriesNextServer:切换实例的重试次数。
7. hello-service.ribbon.MaxAutoRetries:对当前实例的重试次数。
根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由MaxAutoRetries配置),如果不行,就换一个实例进行访问,如果还是不行,再换一次实例访问(更换次数由MaxAutoRetriesNextServer配置),如果依然不行,返回失败信息。