Ribbon客户端负载
客户端负载均衡
简介:
Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,基于Netflix Ribbon实现。
Spring Cloud Ribbon是一个工具类框架,不像服务注册中心,配置中心,API网关那样需要独立部署,但它几乎存在于每一个微服务和基础设施中。
因为微服务的调用,API网关的请求转发等内容实际上都是通过Ribbon来实现的,包括Feign也是基于Ribbon实现的工具。
负载均衡:
负载均衡是对系统的高可用,网络压力的缓解和处理能力扩容的重要手段之一。常说的负载均衡是服务器端负载均衡
服务器端负载均衡分为硬件和软件
硬件负载均衡设备或软件负载均衡设备都会维护一个下挂可用的服务端清单,通过心跳检测来剔除故障的服务端节点以保证清单中都是可正常访问的服务端节点
客户端负载均衡和服务端负载均衡最大的不同点在于上面提到的服务清单所存储的位置。
在客户端负载中,所有客户端节点都维护着自己要访问的服务端清单,这些清单来自于服务注册中心,如Eureka服务端。
客户端负载也需要心跳取维护服务端清单的健康性,需与服务注册中心配合
Spring Cloud实现的服务治理框架中,默认创建针对各个服务治理框架的Ribbon自动化整合配置,如:
Eureka中的org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,
Consul中的org.springframework.cloud.consul.discovery.RibbonConsulAutoConfiguration
查看两类找到配置详情
通过Spring Cloud Ribbon的封装,微服务架构中使用客户端负载均衡调用只需如下两步:
- 服务提供者只需启动多个服务实例并注册到一个注册中心或是多个相关联的服务注册中心
- 服务消费者直接通过调用被@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用
如此即可将服务提供者的高可用以及服务消费者的负载均衡调用一起实现
RestTemplate详解
GET请求
GET请求有两个方法进行调用,getForEntity和getForObject
getForEntity:
该方法返回的是ResponseEntity
该对象是Spring对HTTP请求响应的封装,存储了HTTP的几个重要元素
如HTTP请求状态码的枚举对象HttpStatus(404,500等),
public class ResponseEntity<T> extends HttpEntity<T>,在其父类HttpEntity中还存储着HTTP请求的头信息对象HttpHeaders以及泛型类型的请求体对象
实例:
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://USER-SERVICE/user?name={1}", String.class, "didi");
String body = responseEntity.getBody();
访问助手
若希望返回的body是User对象类型:
ResponseEntity<User> responseEntity = restTemplate.getForEntity("http://USER-SERVICE/user?name={1}", User.class, "didi");
User body = responseEntity.getBody();
方法的三个重载:
getForEntity(String url, Class responseType, Object...urlVariables):
三个参数,url为请求的地址,responseType为请求响应体body(ResponseEntity<responseType>)的包装类型,urlVariables为url中的参数绑定,即url使用占位符{1}配合urlVariables
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}占位符的值
Map<String, String> params = new HashMap<>(){
{
put("name", "data");
}
}
restTemplate.getForEntity("http://USER-SERVICE/user?name={name}", String.class, params);
getForEntity(URI url, Class responseType):
该方法使用URI对象代替url和urlVariables参数来指定访问地址和参数绑定。
UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://USER-SERVICE/user?name={name}")
.build()
.expand("dodo")
.encode();
URI uri = uriComponents.toUri();
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
getForObject:
该方法可理解为对getEntity的进一步封装,它通过HttpMessageConverterExtractor对HTTP的请求响应体body内容进行对象转换,实现请求直接返回包装好的对象内容。(省略ResponseEntity步骤)
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject(uri, String.class);
当body是一个User对象时,
User result = restTemplate.getForObject(uri, User.class);
方法的三个重载:
getForObject(String url, Class responseType, Object...urlVariables):
getForObject(String url, Class responseType, Map urlVariables):
getForObject(URI url, Class responseType):
POST请求
POST请求有三个方法调用,postForEntity,postForObject,postForLocation
postForEntity:
该方法类似getForEntity,调用后返回ResponseEntity对象,其中T为请求响应的body类型。
RestTemplate restTemplate = new RestTemplate();
User user = new User("didi", 30);
ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://USER-SERVICE", user, String.class);
String body = responseEntity.getBody();
方法的三个重载:
postForEntity(String url, Object request, Class responseType, Object...uriVariables)
postForEntity(String url, Object request, Class responseType, Map uriVariables)
postForEntity(URI url, Object request, Class responseType)
以上方法的参数用法大部分与getForEntity一致,除了新增的request参数
request参数可以是一个普通对象,也可以是一个HttpEntity对象,若是一个普通对象,RestTemplate会将请求对象转换为一个HttpEntity对象来处理,其中Object就是request的类型,request内容会被视作完整的body来处理;若request是一个HttpEntity对象,那么就会被当做一个完成的HTTP请求对象来处理,这个request中不仅包含了body的内容也包含了header的内容。
postForObject:
该方法与getForObject类似,省略ResponseEntity的处理,直接将请求响应的body内容包装成对象来返回使用
User user = new User();
String postResult = restTemplate.postForObject("http://USER-SERVICE/user", user, String.class);
方法的三个重载:
postForObject(String url, Object request, Class responseType, Object...uriVariables)
postForObject(String url, Object request, Class responseType, Map uriVariables)
postForObject(URI url, Object request, Class responseType)
postForLocation:
该方法实现了以POST请求提交资源,并返回新资源的URI
User user = new User("didi", 40);
URI responseURI = restTemplate.postForLocation("http://USER-SERVICE/user", user);
方法的三个重载:
postForLocation(String url, Object request, Object...urlVariables)
postForLocation(String url, Object request, Map urlVariables)
postForLocation(URI url, url, Object request)
由于postForLocation函数会返回新资源的URI,该URI就相当于指定了返回类型,所以此方法实现的POST请求不需要像postForEntity和postForObject那样指定responseType
PUT请求
在RestTemplate中,对PUT请求可以通过put方法进行调用实现
RestTemplate restTemplate = new RestTemplate();
Long id = 10001L;
User user = new User("didi", 40);
restTemplate.put("http://USER-SERVICE/user/{1}", user, id);
方法的三个重载:
put(String url, Object request, Object...urlVariables)
put(String url, Object request, MapurlVariables)
put(URI url, Object request)
put方法为void类型,所以没有返回内容,其他用法同postForObject
DELETE请求
在RestTemplate中,对DELETE请求可以通过delete方法进行调用实现
RestTemplate restTemplate = new RestTemplate();
Long id = 10001L;
restTemplate.delete("http://USER-SERVICE/user/{1}", id);
方法的三个重载:
delete(String url, Object...urlVariables)
delete(String url, Map urlVariables)
delete(URI url)
一般DELETE请求将唯一标识拼接在url中,故而DELETE请求也不需要request的body信息
源码分析
执行流程
@LoadBalanced:
分析@LoadBalanced,该注解用于给RestTemplate做标记,以使用负载均衡的客户端(LoadBalancerClient)来配置它
LoadBalancedClient是一个接口:
public interface ServiceInstanceChooser {
ServiceInstance choose(String serviceId);
}
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;
/**
* Create a proper URI with a real host and port for systems to utilize.
* Some systems use a URI with the logical serivce name as the host,
* such as http://myservice/path/to/service. This will replace the
* service name with the host:port from the ServiceInstance.
* @param instance
* @param original a URI with the host as a logical service name
* @return a reconstructed URI
*/
URI reconstructURI(ServiceInstance instance, URI original);
即:服务名调用的url拼接成实例的host:port的url再进行请求
}
从接口定义的抽象方法了解客户端负载均衡器中具备的功能:
ServiceInstance choose(String serviceId):
根据传入的服务名serviceId,从负载均衡器中挑选一个对应服务的实例,return a ServiceInstance that matches the serviceId
T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException:
使用从负载均衡器中挑选出的服务实例来执行请求内容
URI reconstructURI(ServiceInstance instance, URI original):
为系统构建一个合适的host:port形式的URI。在分布式系统中,是引用逻辑上的服务名作为host来构建URI进行请求,参数ServiceInstance对象是带有host和port的具体服务实例,URI对象是使用逻辑服务名定义为host的URI,返回的URI内容则是通过ServiceInstance的服务实例拼接出的具体host:port形式的请求地址
RibbonLoadBalancerClient是LoadBalancerClient的具体实现
在LoadBalancerClient接口的所属包org.springframework.cloud.client.loadbalancer中,LoadBalancerAutoConfiguration为实现客户端负载均衡器的自动化配置类。
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
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 (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
}
};
}
@Configuration
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return new RestTemplateCustomizer() {
@Override
public void customize(RestTemplate restTemplate) {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
}
};
}
}
@ConditionalOnClass(RestTemplate.class):Classpath里有指定的类
@ConditionalOnBean(LoadBalancerClient.class):配置了某个特定Bean
@ConditionalOnMissingBean:没有配置特定的Bean
以上可知:Ribbon实现的负载均衡自动化配置需要满足下面两个条件
@ConditionalOnClass(RestTemplate.class):RestTemplate类必须存在于当前工程的环境中
@ConditionalOnBean(LoadBalancerClient.class):在Spring的Bean工程中必须有LoadBalancerClient的实现Bean
在该自动化配置类中,做了三件事:
-创建了一个LoadBalancerInterceptor的Bean,用于实现对客户端发起请求时进行拦截,以实现客户端负载均衡。
-创建了一个RestTemplateCustomizer的Bean,用于给RestTemplate增加LoadBalancerInterceptor拦截器
-维护了一个被@LoadBalanced注解修饰的RestTemplate对象列表,并在这里进行初始化,通过调用RestTemplateCustomizer的实例来给需要客户端负载均衡的RestTemplate增加LoadBalancerInterceptor拦截器
LoadBalancerInterceptor拦截器:
拦截器将一个普通的RestTemplate变成客户端负载均衡:
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) {
// for backwards compatibility
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();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName,
requestFactory.createRequest(request, body, execution)
等效于:
new LoadBalancerRequest<ClientHttpResponse>() {
@Override
public ClientHttpResponse apply(final ServiceInstance instance)
throws Exception {
HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, loadBalancer);
return execution.execute(serviceRequest, body);
}
}
);
public class ServiceRequestWrapper extends HttpRequestWrapper {
private final ServiceInstance instance;
private final 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;
}
}
}
以上可知,拦截器中注入了LoadBalancerClient的实现,当一个被@LoadBalanced注解修饰的RestTemplate对象向外发起HTTP请求时,会被LoadBalancerInterceptor类的intercept方法拦截。由于使用RestTemplate时采用了服务名作为host,故而直接从HttpRequest的URI对象中通过getHost()便可取到服务名,然后调用execute方法根据服务名来选择实例并发起实际的请求。
RibbonLoadBalancerClient:
org.springframework.cloud.netflix.ribbon
LoadBalancerClient的实现类
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
serviceId), serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
Server server = null;
if(serviceInstance instanceof RibbonServer) {
server = ((RibbonServer)serviceInstance).getServer();
}
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonLoadBalancerContext context = this.clientFactory
.getLoadBalancerContext(serviceId);
RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);
try {
T returnVal = request.apply(serviceInstance);
statsRecorder.recordStats(returnVal);
return returnVal;
}
// catch IOException and rethrow so RestTemplate behaves correctly
catch (IOException ex) {
statsRecorder.recordStats(ex);
throw ex;
}
catch (Exception ex) {
statsRecorder.recordStats(ex);
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
以上,在execute方法的实现中,第一步是通过getServer根据传入的服务名serviceId取获得具体的服务实例:
protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
return loadBalancer.chooseServer("default");
}
getServer的源码可知,此处获取具体服务实例的时候没有使用LoadBalancerClient接口中的choose方法,而是使用了Netflix Ribbon自身的ILoadBalancer接口中定义的chooseServer方法。
ILoadBalancer接口:
/**
* Interface that defines the operations for a software loadbalancer. A typical
* loadbalancer minimally need a set of servers to loadbalance for, a method to
* mark a particular server to be out of rotation and a call that will choose a
* server from the existing list of server.
*
* @author stonse
*
*/
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();
}
该接口中定义了一个客户端负载均衡器所需的抽象操作
addServers:向负载均衡器中维护的实例列表增加服务实例
chooseServer:通过某种策略,从负载均衡器中挑选出一个具体的服务实例
markServerDown:用来通知和标识负载均衡器中某个具体实例已经停止服务,不然负载均衡器在下一次获取服务实例清单前都会认为服务实例均是正常服务的
getReachableServers:获取当前正常服务的实例列表
getAllServers:获取所有已知的服务实例列表,包括正常服务和停止服务的实例
该接口定义中设计的Server对象定义是一个传统的方服务段端节点,在该类中存储了服务端节点的一些元数据信息,包括host,port以及一些部署信息等
ILoadBalancer接口的实现
BaseLoadBalancer类实现了基础的负载均衡
DynamicServerListLoadBalancer和ZoneAwareLoadBalancer在负载均衡的策略上做了功能的扩展
1.
public abstract class AbstractLoadBalancer implements ILoadBalancer
2.
public class NoOpLoadBalancer extends AbstractLoadBalancer
public class BaseLoadBalancer extends AbstractLoadBalancer implements PrimeConnections.PrimeConnectionListener, IClientConfigAware
3.
public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer
4.
public class ZoneAwareLoadBalancer<T extends Server> extends DynamicServerListLoadBalancer<T>
通过RibbonClientConfiguration配置类可知,整合Ribbon时默认采用了ZoneAwareLoadBalancer
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
return this.propertiesFactory.get(ILoadBalancer.class, config, name);
}
// 此处默认返回
return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
serverListFilter, serverListUpdater);
}
回到execute,通过服务名serviceId调用getServer(ILoadBalancer loadBalancer)
通过ZoneAwareLoadBalancer的chooseServer方法获取负载均衡策略分配到的服务实例对象Server
再将其包装成RibbonLoadBalancerClient的静态内部类RibbonServer
该对象除了存储了服务实例的信息之外,还增加服务名serviceId,是否需要使用HTTPS等其他信息
继而使用RibbonServer再回调LoadBalancerInterceptor请求拦截器中LoadBalancerRequest的apply(ServiceInstance instance),向一个实际的具体服务实例发起请求,从而实现一开始以服务名为host的URI请求到host:port形式的实际访问地址的转换
以下接口暴露了服务治理系统中每个服务实例需要提供的一些基本信息,如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类型的元数据集合。
public static class RibbonServer implements ServiceInstance {
private final String serviceId;
private final Server server;
private final boolean secure;
private Map<String, String> metadata;
public RibbonServer(String serviceId, Server server) {
this(serviceId, server, false, Collections.<String, String> emptyMap());
}
public RibbonServer(String serviceId, Server server, boolean secure,
Map<String, String> metadata) {
this.serviceId = serviceId;
this.server = server;
this.secure = secure;
this.metadata = metadata;
}
...
}
调用LoadBalancerRequest的apply(),接收到实例后,如何通过LoadBalancerClient接口中的reconstructURI操作组织具体请求地址的?
在apply()中将request包装成ServiceRequestWrapper,该对象继承HttpReqquestWrapper并重写getURI方法,重写后的getURI,在Wrapper中拼接URI
public LoadBalancerRequest<ClientHttpResponse> createRequest(final HttpRequest request,
final byte[] body, final ClientHttpRequestExecution execution) {
return new LoadBalancerRequest<ClientHttpResponse>() {
@Override
public ClientHttpResponse apply(final ServiceInstance instance)
throws Exception {
// 此处ServiceRequestWrapper对象
HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, loadBalancer);
if (transformers != null) {
for (LoadBalancerRequestTransformer transformer : transformers) {
serviceRequest = transformer.transformRequest(serviceRequest, instance);
}
}
return execution.execute(serviceRequest, body);
}
};
}
通过调用LoadBalancerClient接口的reconstructURI方法来重构一个URI来进行访问
public class ServiceRequestWrapper extends HttpRequestWrapper {
private final ServiceInstance instance;
private final LoadBalancerClient loadBalancer;
@Override
public URI getURI() {
// 此处重构URI
URI uri = this.loadBalancer.reconstructURI(
this.instance, getRequest().getURI());
return uri;
}
拼接URI中,调用RibbonLoadBalancerClient.reconstructURI()
@Override
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.getPort());
IClientConfig clientConfig = clientFactory.getClientConfig(serviceId);
ServerIntrospector serverIntrospector = serverIntrospector(serviceId);
URI uri = RibbonUtils.updateToHttpsIfNeeded(original, clientConfig,
serverIntrospector, server);
return context.reconstructURIWithServer(server, uri);
}
拼接URI中,调用RibbonLoadBalancerContext.reconstructURIWithServer()[在上一步通过ServiceInstance实例对象的serviceId从SpringClientFactory类的clientFactory对象中获取对应的serviceId的负载均衡的上下文RibbonLoadBalancerContext对象,继而根据ServiceInstance中的信息构建具体服务实例信息的Server对象,并调用reconstructURIWithServer()方法]
public class LoadBalancerContext implements IClientConfigAware {
public URI reconstructURIWithServer(Server server, URI original) {
String host = server.getHost();
int port = server.getPort();
String scheme = server.getScheme();
if (host.equals(original.getHost())
&& port == original.getPort()
&& scheme == original.getScheme()) {
return original;
}
if (scheme == null) {
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);
}
}
以上可知,reconstructURI第一个保存具体服务实例的参数使用了SpringCloud定义的ServiceInstance,而reconstructURIWithServer中使用了Netflix中定义的Server
故而在RibbonLoadBalancerClient实现reconstructURI时,做了一次转换,使用ServiceInstance的host和port构建了一个Server对象给reconstructURIWithServer使用。
从reconstructURIWithServer实现逻辑中看出,它从Server对象获取host和port信息,然后根据以服务名为host的URI对象original中获取其他请求信息,将两者内容进行拼接整合,形成要访问的服务实例的具体地址。
回调拦截器调用中,执行了具体的请求之后,Ribbon通过RibbonStatsRecorder对象对服务的请求进行了跟踪记录。
SpringClientFactory和RibbonLoadBalancerContext:
- SpringClientFactory类是一个用来创建客户端负载均衡器的工厂类,该工厂类会为每一个不同名的Ribbon客户端生成不同的Spring上下文
- RibbonLoadBalancerContext类是LoadBalancerContext的子类,该类用于存储一些被负载均衡器使用的上下文内容和API操作(reconstructURIWithServer即其中之一)
总结:
1 通过LoadBalancerInterceptor拦截器对RestTemplate的请求进行拦截
2 利用SpringCloud的负载均衡器LoadBalancerClient将以逻辑服务名为host的URI转换成具体的服务实例地址
3 Ribbon实现负载均衡器时,实际上使用的还是Ribbon定义的ILoadBalancer接口的实现,自动化配置默认采用ZoneAwareLoadBalancer的实例实现客户端负载均衡
负载均衡器
AbstractLoadBalancer:
AbstractLoadBalancer是ILoadBalancer接口的抽象实现。
在该抽象类中定义了一个关于服务实例的分组枚举类ServerGroup,包含以下三种不同类型
- ALL: 所有服务实例
- STATUS_UP: 正常服务的实例
- STATUS_NOT_UP: 停止服务的实例
除此之外AbstractLoadBalancer实现了chooseServer()方法,该方法通过调用接口中的chooseServer(Object key)实现,其中参数key为null,表示在选择具体服务实例时忽略key条件判断。
定义了两个抽象方法:
- getServerList(ServerGroup serverGroup):定义了根据分组类型来获取不同服务实例的列表
- 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();
}
BaseLoadBalancer:
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];
logger.debug("LoadBalancer: PingTask executing [{}] servers configured", numCandidates);
for (int i = 0; i < numCandidates; i++) {
results[i] = false; /* Default answer is DEAD. */
try {
if (ping != null) {
results[i] = ping.isAlive(servers[i]);
}
} catch (Exception e) {
logger.error("Exception while pinging Server: '{}'", servers[i], e);
}
}
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 (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}
6 启动ping任务:在BaseLoadBalancer的默认构造方法中,会直接启动一个用于定时检查Server是否健康的任务。默认执行间隔10秒
7 实现了ILoadBalancer接口定义的负载均衡器的基本操作:
- 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("LoadBalancer [{}]: Exception while adding Servers", name, e);
}
}
}
8 chooseServer(Object key):挑选一个具体的服务实例
9 markServerDown(Server server):标记某个服务实例暂停服务
public void markServerDown(Server server) {
if (server == null || !server.isAlive()) {
return;
}
logger.error("LoadBalancer [{}]: markServerDown called on [{}]", name, server.getId());
server.setAlive(false);
// forceQuickPing();
notifyServerStatusChangeListener(singleton(server));
}
10 getReachableServers():获取可用的服务实例列表。由于BaseLoadBalancer中单独维护了一个正常服务的实例清单,所以直接返回即可。
public List<Server> getReachableServers() {
return Collections.unmodifiableList(upServerList);
}
11 getAllServers():获取所有的服务实例列表。由于BaseLoadBalancer中单独维护了一个所有服务的实例清单,亦可直接返回
public List<Server> getAllServers() {
return Collections.unmodifiableList(allServerList);
}
DynamicServerListLoadBalancer
DynamicServerListLoadBalancer类继承于BaseLoadBalancer类,对基础负载均衡器的扩展。实现了服务实例清单在运行期的动态更新能力;同时具备对服务实例清单的过滤功能。
- ServerList:服务列表的操作对象ServerList serverListImpl,T为Server的子类,即代表了一个具体的服务实例的扩展类
public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer {
boolean isSecure = false;
boolean useTunnel = false;
// to keep track of modification of server lists
protected AtomicBoolean serverListUpdateInProgress = new AtomicBoolean(false);
volatile ServerList<T> serverListImpl;
volatile ServerListFilter<T> filter;
protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
@Override
public void doUpdate() {
updateListOfServers();
}
};
protected volatile ServerListUpdater serverListUpdater;
...
}
public interface ServerList<T extends Server> {
// 用于获取初始化的服务实例清单
public List<T> getInitialListOfServers();
// 获取更新的服务实例清单
public List<T> getUpdatedListOfServers();
}
ServerList的实现类有:
StaticServerList
DomainExtractingServerList
AbstractServerList
ConfigurationBasedServerList
DiscoveryEnabledNIWSServerList
DynamicServerLoadBalancer中的ServerList默认配置分析SpringCloud整合Ribbon和Eureka的包org.springframework.cloud.netflix.ribbon.eureka下进行探索
配置类EurekaRibbonClientConfiguration
@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(IClientConfig config, Provider<EurekaClient> eurekaClientProvider) {
if (this.propertiesFactory.isSet(ServerList.class, this.serviceId)) {
return (ServerList)this.propertiesFactory.get(ServerList.class, config, this.serviceId);
} else {
DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(config, eurekaClientProvider);
DomainExtractingServerList serverList = new DomainExtractingServerList(discoveryServerList, config, this.approximateZoneFromHostname);
return serverList;
}
}
此处创建一个DomainExtractingServerList实例且其内部定义了一个ServerLIst list。
同时DomainExtractingServerLIst对getInitialListOfServers和getUpdatedListOfServers的具体实现委托给了内部定义的ServerList list对象
ServerList list是通过创建DomainExtractingServerList时,由构造函数传入的DiscoveryEnabledNIWWSServerList实现
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;
}
public List<DiscoveryEnabledServer> getInitialListOfServers() {
List<DiscoveryEnabledServer> servers = this.setZones(this.list.getInitialListOfServers());
return servers;
}
public List<DiscoveryEnabledServer> getUpdatedListOfServers() {
List<DiscoveryEnabledServer> servers = this.setZones(this.list.getUpdatedListOfServers());
return servers;
}
...
}
而DiscoveryEnabledNIWWSServerList通过该类中的一个私有方法obtainServersViaDiscovery通过服务发现机制实现服务实例的获取
public List<DiscoveryEnabledServer> getInitialListOfServers() {
return this.obtainServersViaDiscovery();
}
public List<DiscoveryEnabledServer> getUpdatedListOfServers() {
return this.obtainServersViaDiscovery();
}
obtainServersViaDiscovery:
通过EurekaClient从服务注册中心获取到具体的服务实例InstanceInfo列表(EurekaClient的具体实现,传入的vipAddress为逻辑上的服务名,如USER-SERVICE)。继而遍历服务实例,将状态为UP的实例转换成DiscoveryEnabledServer对象,最后将这些实例组织成列表返回。
private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
List<DiscoveryEnabledServer> serverList = new ArrayList();
if (this.eurekaClientProvider != null && this.eurekaClientProvider.get() != null) {
EurekaClient eurekaClient = (EurekaClient)this.eurekaClientProvider.get();
if (this.vipAddresses != null) {
String[] var3 = this.vipAddresses.split(",");
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
String vipAddress = var3[var5];
List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, this.isSecure, this.targetRegion);
Iterator var8 = listOfInstanceInfo.iterator();
while(var8.hasNext()) {
InstanceInfo ii = (InstanceInfo)var8.next();
if (ii.getStatus().equals(InstanceStatus.UP)) {
if (this.shouldUseOverridePort) {
if (logger.isDebugEnabled()) {
logger.debug("Overriding port on client name: " + this.clientName + " to " + this.overridePort);
}
InstanceInfo copy = new InstanceInfo(ii);
if (this.isSecure) {
ii = (new Builder(copy)).setSecurePort(this.overridePort).build();
} else {
ii = (new Builder(copy)).setPort(this.overridePort).build();
}
}
DiscoveryEnabledServer des = new DiscoveryEnabledServer(ii, this.isSecure, this.shouldUseIpAddr);
des.setZone(DiscoveryClient.getZone(ii));
serverList.add(des);
}
}
if (serverList.size() > 0 && this.prioritizeVipAddressBasedServers) {
break;
}
}
}
return serverList;
} else {
logger.warn("EurekaClient has not been initialized yet, returning an empty list");
return new ArrayList();
}
}
服务实例清单的List列表返回到DomainExtractingServerList类中,继续通过setZones方法进行处理:
将List列表中元素转换成内部定义的DiscoveryEnabledServer的子类对象DomainExtractingServer,且在该对象的构造器中为服务实例对象设置一些必要属性:id,zone,isAliiveFlag,readyToServer等
public class DomainExtractingServerList implements ServerList<DiscoveryEnabledServer> {
private List<DiscoveryEnabledServer> setZones(List<DiscoveryEnabledServer> servers) {
List<DiscoveryEnabledServer> result = new ArrayList();
boolean isSecure = this.clientConfig.getPropertyAsBoolean(CommonClientConfigKey.IsSecure, Boolean.TRUE.booleanValue());
boolean shouldUseIpAddr = this.clientConfig.getPropertyAsBoolean(CommonClientConfigKey.UseIPAddrForServer, Boolean.FALSE.booleanValue());
Iterator var5 = servers.iterator();
while(var5.hasNext()) {
DiscoveryEnabledServer server = (DiscoveryEnabledServer)var5.next();
result.add(new DomainExtractingServer(server, isSecure, shouldUseIpAddr, this.approximateZoneFromHostname));
}
return result;
}
...
}
- ServerListUpdater:向EurekaServer获取服务实例清单以及在获取服务实例清单后更新本地的服务实例清单。
protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
@Override
public void doUpdate() {
updateListOfServers();
}
};
protected volatile ServerListUpdater serverListUpdater;
serverListUpdater对象实现对ServerLIst的更新,该对象内部定义了一个UpdateAction接口,上面就是以匿名内部类的方式创建了一个具体实现,其中doUpdate实现的内容就是对ServerList的具体更新操作。此外,serverListUpdater该定义了一系列控制它和获取它的信息的操作
public interface ServerListUpdater {
public interface UpdateAction {
void doUpdate();
}
/**
* start the serverList updater with the given update action UpdateAction对象为更新操作的具体实现
*/
void start(UpdateAction updateAction);
/**
* stop the serverList updater. This call should be idempotent 停止服务更新器
*/
void stop();
/**
* @return the last update timestamp as a {@link java.util.Date} string 最近的更新时间戳
*/
String getLastUpdate();
/**
* @return the number of ms that has elapsed since last update 获取上一次更新到现在的时间间隔,单位毫秒
*/
long getDurationSinceLastUpdateMs();
/**
* @return the number of update cycles missed, if valid 获取错过的更新周期数
*/
int getNumberMissedCycles();
/**
* @return the number of threads used, if vaid 获取核心线程数
*/
int getCoreThreads();
}
ServerListUpdater的实现类:
EurekaNotificationServerListUpdater:
该更新器服务于DynamicServerListLoadBalancer负载均衡器,它的触发机制与PollingServerListUpdater不同,需要利用Eureka的事件监听器来驱动服务列表的更新操作
PollingServerListUpdater:
动态服务列表更新的默认策略,DynamicServerListLoadBalancer负载均衡器中的默认实现,它通过定时任务的方式进服务列表的更新。
定时任务initialDelayMs为1000,refreshIntervalMs为30*1000,即更新服务实例在初始化之后延迟1秒后开始执行,并以间隔30秒为周期重复执行
- ServerListFilter
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);
}
ServerListFilter接口定义了一个方法List getFilteredListOfServers(List servers),用于实现对服务实例列表的过滤。实现如下:
AbstractServerListFilter
ZoneAffinityServerListFilter
ZonePreferenceServerListFilter
DefaultNIWSServerListFilter
ServerListSubsetFilter
除了ZonePreferenceServerListFilter的实现时SpringCloudRibbon中对NetflixRibbon的扩展实现外,其他均是NetflixRibbon中的原生实现类
AbstractServerListFilter:抽象过滤器,定义了过滤时需要的对象LoadBalancerStats
public abstract class AbstractServerListFilter<T extends Server> implements ServerListFilter<T> {
private volatile LoadBalancerStats stats;
public void setLoadBalancerStats(LoadBalancerStats stats) {
this.stats = stats;
}
public LoadBalancerStats getLoadBalancerStats() {
return stats;
}
}
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())来实现的比较。
由shouldEnableZoneAffinity方法判断是否要启用"区域感知"功能
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;
}
}
}
若以下三个有一个条件符合则不启用区域感知过滤的服务实例清单
blackOutServerPercentage:故障实例百分比(断路器断开数/实例数量)>=0.8
activeRequestsPerServer:实例平均负载 >=0.6
availableServers:可用实例数(实例数量 - 断路器断开数)<2
可关联Eureka对于区域分配设计来保证跨区域故障的高可用问题
DefaultNIWSServerListFilter:该过滤器完全继承自ZoneAffinityServerListFilter,是默认的NIWS(Netflix Internal Web Service)过滤器
ServerListSubsetFilter:该过滤器也继承自ZoneAffinityServerListFilter,适用于拥有大规模服务器集群的系统。因为它可以产生一个区域感知结果的子集列表,同时它还能够通过比较服务实例的通信失败数量和并发连接数来判定该服务是否健康来选择性地从服务实例列表中剔除那些相对不够健康的实例。
该过滤器的实现主要分为以下三步:
1.获取"区域感知"的过滤结果,作为候选的服务实例清单
2.从当前消费者维护的服务实例子集中剔除那些相对不够健康的实例(同时从候选清单中剔除,防止第三步又被选入),不够健康的标准如下:
a.服务实例的并发连接数超过客户端配置的值,默认为0,配置参数为
<clientName>.<nameSpace>.ServerListSubsetFilter.eliminationConnectionThresold
b.服务实例的失败数超过客户端配置的值,默认为0,配置参数为
<clientName>.<nameSpace>.ServerListSubsetFilter.eliminationFailureThresold
c.若按符合上面任一规则的服务实例剔除后,剔除比例小于客户端默认配置的百分比,默认为0.1(10%),配置参数为
<clientName>.<nameSpace>.ServerListSubsetFilter.forceEliminatePercent,那么就先对剩下的实例列表进行健康排序,再从不健康的实例进行剔除,直到达到配置的剔除百分比。
3.在完成剔除后,清单已经少了至少10%(默认值)的服务实例,最后通过随机的方式从候选清单中选出一批实例加入到清单中,以保持服务实例子集与原来的数量一致,而默认的实例子集数量为20,其配置参数为
<clientName>.<nameSpace>.ServerListSubsetFilter.size
ZonePreferenceServerListFilter:若使用SpringCloud整合Eureka和Ribbon时会默认使用该过滤器。
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;
}
通过父类的过滤器获得区域感知的服务实例列表,遍历这个结果取出根据消费者配置预设的区域Zone进行过滤,若过滤的结果为空就返回父类获取的结果,不为空返回通过消费者配置的Zone过滤后的结果
ZoneAwareLoadBalancer:
DynamicServerListLoadBalancer(么有重写chooseServer,采用BaseLoadBalancer中实现的算法,使用RoundRobinRule规则,以线性轮询的方式选择调用的服务实例,该算法没有Zone概念)的扩展。重写了setServerListForZones
protected void setServerListForZones(Map<String, List<Server>> zoneServersMap) {
super.setServerListForZones(zoneServersMap);
if (balancers == null) {
// 存储每个Zone区域对应的负载均衡器。
balancers = new ConcurrentHashMap<String, BaseLoadBalancer>();
}
for (Map.Entry<String, List<Server>> entry: zoneServersMap.entrySet()) {
String zone = entry.getKey().toLowerCase();
// 具体的负载均衡器的创建由getLoadBalancer完成,同时在创建负载均衡器的时候会创建它的规则IRule(若没有实例就创建一个AvailabilityFilteringRule规则)
// 创建完负载均衡器后马上调用setServersList方法为其设置对应Zone区域的实例清单
getLoadBalancer(zone).setServersList(entry.getValue());
}
// check if there is any zone that no longer has a server
// and set the list to empty so that the zone related metrics does not
// contain stale data
// 对Zone区域中实例清单的检查,若Zone区域下没有实例了将其balancers中对应Zone区域的实例列表清空,防止过时的Zone区域统计信息干扰具体实例的选择算法
for (Map.Entry<String, BaseLoadBalancer> existingLBEntry: balancers.entrySet()) {
if (!zoneServersMap.keySet().contains(existingLBEntry.getKey())) {
existingLBEntry.getValue().setServersList(Collections.emptyList());
}
}
}
重写了chooseServer,当负载均衡器中维护的实例所属的Zone区域的个数大于1时执行选择策略,否则使用父类的实现。
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();
// 为当前负载均衡器中所有的Zone区域分别创建快照,保存在Map中
Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
logger.debug("Zone snapshots: {}", zoneSnapshot);
if (triggeringLoad == null) {
triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d);
}
if (triggeringBlackoutPercentage == null) {
triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d);
}
// 获取可用的Zone区域集合,在该函数中会通过Zone区域快照中的统计数据来实现可用区的挑选。
// 首先会剔除符合这些规则的Zone区域:所属实例数为零的Zone区域;Zone区域内实例的平均负载小于零,或实例故障率(断路器断开次数/实例数)大于等于阈值(默认0.99999)
// 然后根据Zone区域的实例平均负载计算出最差的Zone区域,实力平均负载最高的Zone区域
// 若在上面的过程中没有符合剔除要求的区域,同时实例最大平均负载小于阈值(默认为20%),就直接返回所有Zone区域为可用区域。否则从最坏Zone区域集合中随机选择一个,将它从可用Zone区域集合中剔除
// 当获得的可用Zone区域集合不为空,且个数小于Zone区域总数,就随机选择一个Zone区域
// 在确定了某个Zone区域后,则获取了对应Zone区域的服务均衡器,并调用chooseServer来选择具体的服务实例,而在chooseServer中将使用IRule接口的choose方法来选择具体的服务实例。在这里,IRule接口的实现会使用ZoneAvoidanceRule来挑选出具体的服务实例
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
logger.debug("Available zones: {}", availableZones);
if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) {
String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
logger.debug("Zone chosen: {}", zone);
if (zone != null) {
BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
server = zoneLoadBalancer.chooseServer(key);
}
}
} catch (Exception e) {
logger.error("Error choosing server using zone aware logic for load balancer={}", name, e);
}
if (server != null) {
return server;
} else {
logger.debug("Zone avoidance logic is not invoked.");
return super.chooseServer(key);
}
}
负载均衡策略
IRule
IRule实现类:
- AbstractLoadBalancerRule
-
- ClientConfigEnabledRoundRobinRule
-
-
- BestAvailableRule
-
-
-
- PredicateBasedRule
-
-
-
-
- AvailabilityFilteringRule
-
-
-
-
-
- ZoneAvoidanceRule
-
-
-
- RoundRobinRule
-
-
- WeightedResponseTimeRule
-
-
- RandomRule
-
- RetryRule
public interface IRule{
/*
* choose one alive server from lb.allServers or
* lb.upServers according to key
*
* @return choosen Server object. NULL is returned if none
* server is available
*/
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}
AbstractLoadBalancerRule
负载均衡策略的抽象类,该抽象类中定义了负载均衡器ILoadBalancer对象,该对象能够在具体实现选择服务策略时,获取到负载均衡器中维护的信息来作为分配依据,并以此设计一些算法来实现针对特定场景的高效策略
public abstract class AbstractLoadBalancerRule implements IRule, IClientConfigAware {
private ILoadBalancer lb;
@Override
public void setLoadBalancer(ILoadBalancer lb){
this.lb = lb;
}
@Override
public ILoadBalancer getLoadBalancer(){
return lb;
}
}
RandomRule
该策略实现了从服务实例清单中随机选择一个服务实例的功能。
public class RandomRule extends AbstractLoadBalancerRule {
Random rand;
public RandomRule() {
rand = new Random();
}
/**
* Randomly choose from all living servers
*/
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
//选择逻辑在while中,若获取不到服务实例则死循环,存在并发bug
while (server == null) {
if (Thread.interrupted()) {
return null;
}
// 利用负载均衡器lb获取可用实例列表upList和所有实例列表allList
List<Server> upList = lb.getReachableServers();
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
/*
* No servers. End regardless of pass, because subsequent passes
* only get more restrictive.
*/
return null;
}
// 利用随机数作为可用实例列表的索引值
int index = rand.nextInt(serverCount);
server = upList.get(index);
if (server == null) {
/*
* The only time this should happen is if the server list were
* somehow trimmed. This is a transient condition. Retry after
* yielding.
*/
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
// Shouldn't actually happen.. but must be transient or a bug.
server = null;
Thread.yield();
}
return server;
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
// TODO Auto-generated method stub
}
}
RoundRobinRule
该策略实现了按照线性轮询的方式依次选择每个服务实例的功能。与RandomRule不同的是,RoundRobinRule的选择策略有一个计数器count,若一直选择不到server超过10次就会结束尝试打印信息。
public class RoundRobinRule extends AbstractLoadBalancerRule {
// 线性轮询的实现对象
private AtomicInteger nextServerCyclicCounter;
private static final boolean AVAILABLE_ONLY_SERVERS = true;
private static final boolean ALL_SERVERS = false;
private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class);
public RoundRobinRule() {
nextServerCyclicCounter = new AtomicInteger(0);
}
public RoundRobinRule(ILoadBalancer lb) {
this();
setLoadBalancer(lb);
}
// 此处是选择策略的关键
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
if ((upCount == 0) || (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) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}
/**
* Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
*
* @param modulo The modulo to bound the value of the counter.
* @return The next value.
*/
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
RetryRule
该策略实现了一个具备重试机制的实例选择功能。
public class RetryRule extends AbstractLoadBalancerRule {
// 内部定义了一个IRule对象,默认使用RoundRobinRule实例
IRule subRule = new RoundRobinRule();
long maxRetryMillis = 500;
public RetryRule() {
}
public RetryRule(IRule subRule) {
this.subRule = (subRule != null) ? subRule : new RoundRobinRule();
}
public RetryRule(IRule subRule, long maxRetryMillis) {
this.subRule = (subRule != null) ? subRule : new RoundRobinRule();
this.maxRetryMillis = (maxRetryMillis > 0) ? maxRetryMillis : 500;
}
public void setRule(IRule subRule) {
this.subRule = (subRule != null) ? subRule : new RoundRobinRule();
}
public IRule getRule() {
return subRule;
}
public void setMaxRetryMillis(long maxRetryMillis) {
if (maxRetryMillis > 0) {
this.maxRetryMillis = maxRetryMillis;
} else {
this.maxRetryMillis = 500;
}
}
public long getMaxRetryMillis() {
return maxRetryMillis;
}
@Override
public void setLoadBalancer(ILoadBalancer lb) {
super.setLoadBalancer(lb);
subRule.setLoadBalancer(lb);
}
/*
* choose方法中实现了对内部定义的策略进行反复尝试的策略,若期间能够选择到具体的服务实例就返回,若选择不到就根据设置的尝试结束时间为阈值(maxRetryMillis参数定义的值+choose方法开始执行的时间戳),当超过该阈值就返回null
* Loop if necessary. Note that the time CAN be exceeded depending on the
* subRule, because we're not spawning additional threads and returning
* early.
*/
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)) {
/* pause and retry hoping it's transient */
Thread.yield();
} else {
break;
}
}
task.cancel();
}
if ((answer == null) || (!answer.isAlive())) {
return null;
} else {
return answer;
}
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
WeightedResponseTimeRule
该策略是RoundRobinRule的扩展,增加了根据实例的运行情况来计算权重,并根据权重来选择实例;三个核心内容:定时任务,权重计算,实例选择
定时任务:
该类在初始化时启动定时任务,计算每个服务实例的权重,默认30秒执行一次
void initialize(ILoadBalancer lb) {
if (serverWeightTimer != null) {
serverWeightTimer.cancel();
}
serverWeightTimer = new Timer("NFLoadBalancer-serverWeightTimer-"
+ name, true);
// 此方法启动定时任务
serverWeightTimer.schedule(new DynamicServerWeightTask(), 0,
serverWeightTaskTimerInterval);
// do a initial run
ServerWeight sw = new ServerWeight();
sw.maintainWeights();
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
logger
.info("Stopping NFLoadBalancer-serverWeightTimer-"
+ name);
serverWeightTimer.cancel();
}
}));
}
// 定时任务的内容
class DynamicServerWeightTask extends TimerTask {
public void run() {
ServerWeight serverWeight = new ServerWeight();
try {
serverWeight.maintainWeights();
} catch (Exception e) {
logger.error("Error running DynamicServerWeightTask for {}", name, e);
}
}
}
权重计算:
存储权重的对象List accumulatedWeights = new ArrayList(),该List中每个权重值所处的位置对应了负载均衡器维护的服务实例清单中所有实例在清单中的位置
class ServerWeight {
public void maintainWeights() {
ILoadBalancer lb = getLoadBalancer();
if (lb == null) {
return;
}
if (!serverWeightAssignmentInProgress.compareAndSet(false, true)) {
return;
}
try {
logger.info("Weight adjusting job started");
AbstractLoadBalancer nlb = (AbstractLoadBalancer) lb;
LoadBalancerStats stats = nlb.getLoadBalancerStats();
if (stats == null) {
// no statistics, nothing to do
return;
}
// 所有实例的总平均响应时间totalResponseTime 用于之后的权重计算
double totalResponseTime = 0;
// find maximal 95% response time
for (Server server : nlb.getAllServers()) {
// this will automatically load the stats if not in cache
ServerStats ss = stats.getSingleServerStat(server);
totalResponseTime += ss.getResponseTimeAvg();
}
// weight for each server is (sum of responseTime of all servers - responseTime)
// so that the longer the response time, the less the weight and the less likely to be chosen
// 初始化为0,计算好一个权重需累加到weightSoFar上供下一次计算使用
Double weightSoFar = 0.0;
// create new list and hot swap the reference
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 (Exception e) {
logger.error("Error calculating server weights", e);
} finally {
serverWeightAssignmentInProgress.set(false);
}
}
}
实现步骤:
1.计算所有实例的总平均响应时间totalResponseTime
2.为负载均衡器中维护的实例清单逐个计算权重,计算规则:weightSoFar+totalResponseTime-实例的平均响应时间,权重保存在accumulatedWeights
weightSoFar为前一个实例的权重值,初始化为0
[0,100] 平均响应时间125 总平均响应时间225
(100,250] 平均响应时间75
(200,400) 平均响应时间25 最后是开区间由生成randomWeight的算法决定
注意:每一个实例的区间下限是上一个实例的区间上限,而每一个实例的区间上限是就算并存储于List accumulatedWeights中的权重值;每个区间的宽度就是总的平均响应时间-实例的平均响应时间;实例的平均响应时间越短,权重的宽度越大,越容易被选中
实例选择:
选择实例的核心过程:
- 生成一个[0, 最大权重值)区间内的随机数 randomWeight
- 遍历权重列表,比较权重值与随机数的大小,若权重值大于等于随机数,则取当前权重列表的索引值去服务实例列表中获取具体的实例。
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
// get hold of the current reference in case it is changed from the other thread
List<Double> currentWeights = accumulatedWeights;
if (Thread.interrupted()) {
return null;
}
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}
int serverIndex = 0;
// last one in the list is the sum of all weights
// 获取最后一个实例的权重
double maxTotalWeight = currentWeights.size() == 0 ? 0 : currentWeights.get(currentWeights.size() - 1);
// No server has been hit yet and total weight is not initialized
// fallback to use round robin
// 若最后一个实例的权重值小于0.001,则采用父类实现的线性轮询的策略
if (maxTotalWeight < 0.001d) {
server = super.choose(getLoadBalancer(), key);
if(server == null) {
return server;
}
} else {
// generate a random weight between 0 (inclusive) to maxTotalWeight (exclusive)
// 否则,产生一个[0, maxTotalWeight)的随机数
double randomWeight = random.nextDouble() * maxTotalWeight;
// pick the server index based on the randomIndex
int n = 0;
for (Double d : currentWeights) {
// 遍历维护的权重清单,若权重大于等于随机得到的数值,就选这个实例
if (d >= randomWeight) {
serverIndex = n;
break;
} else {
n++;
}
}
server = allList.get(serverIndex);
}
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
// Next.
server = null;
}
return server;
}
ClientConfigEnabledRoundRobinRule
该策略一般不直接使用,其内部定义了RoundRobinRule策略,并使用了choose方法的线性轮询实现,功能与RoundRobinRule相同;不过可以作为高级策略的扩展。
public class ClientConfigEnabledRoundRobinRule extends AbstractLoadBalancerRule {
RoundRobinRule roundRobinRule = new RoundRobinRule();
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
roundRobinRule = new RoundRobinRule();
}
@Override
public void setLoadBalancer(ILoadBalancer lb) {
super.setLoadBalancer(lb);
roundRobinRule.setLoadBalancer(lb);
}
@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 class BestAvailableRule extends ClientConfigEnabledRoundRobinRule {
private LoadBalancerStats loadBalancerStats;
@Override
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;
}
}
@Override
public void setLoadBalancer(ILoadBalancer lb) {
super.setLoadBalancer(lb);
if (lb instanceof AbstractLoadBalancer) {
loadBalancerStats = ((AbstractLoadBalancer) lb).getLoadBalancerStats();
}
}
}
该choose的核心依据是统计对象loadBalancerStats,当其为空时,该策略无法执行,转而采用父类线性轮询策略
PredicateBasedRule
抽象策略,继承了ClientConfigEnabledRoundRobinRule,且基于Predicate实现的策略
public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
/**
* Method that provides an instance of {@link AbstractServerPredicate} to be used by this class.
* 此抽象方法获取AbstractServerPredicate对象的实现
*/
public abstract AbstractServerPredicate getPredicate();
/**
* Get a server by calling {@link AbstractServerPredicate#chooseRandomlyAfterFiltering(java.util.List, Object)}.
* The performance for this method is O(n) where n is number of servers to be filtered.
*/
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
// 以下方法选出具体的服务实例 即从子类中实现的Predicate逻辑过滤后的服务实例中以线性轮询的方式选出一个
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
}
chooseRoundRobinAfterFiltering方法的实现逻辑:通过内部定义的getEligibleServers方法获取备选的实例清单(实现了过滤),返回清单为空则Option.absent()表示不存在,反之以线性轮询的方式从备选清单中获取一个实例
public abstract class AbstractServerPredicate implements Predicate<PredicateKey> {
protected IRule rule;
private volatile LoadBalancerStats lbStats;
private final Random random = new Random();
private final AtomicInteger nextIndex = new AtomicInteger();
private final Predicate<Server> serverOnlyPredicate = new Predicate<Server>() {
@Override
public boolean apply(@Nullable Server input) {
return AbstractServerPredicate.this.apply(new PredicateKey(input));
}
};
/**
* Choose a server in a round robin fashion after the predicate filters a given list of servers and load balancer key.
*/
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(incrementAndGetModulo(eligible.size())));
}
/**
* Get servers filtered by this predicate from list of servers.
*/
public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) {
if (loadBalancerKey == null) {
return ImmutableList.copyOf(Iterables.filter(servers, this.getServerOnlyPredicate()));
} else {
List<Server> results = Lists.newArrayList();
for (Server server: servers) {
// 遍历服务清单,使用this.apply方法判断实例是否需要保留,若是则添加到结果列表中
if (this.apply(new PredicateKey(loadBalancerKey, server))) {
results.add(server);
}
}
return results;
}
}
==注意:==AbstractServerPredicate实现了com.google.common.base.Predicate接口,而apply方法是该接口中的定义,主要用于实现过滤条件的判断逻辑,chooseRoundRobinAfterFiltering方法只是定义了一个模板策略:
先过滤清单,再轮询选择
如何过滤?需要我们在AbstractServerPredicate的子类中实现apply方法来确定具体的过滤策略
之后的两个策略就是基于此抽象策略实现。
AvailabilityFilteringRule
该策略继承自上面的抽象策略PredicateBasedRule,故而也继承了先过滤清单再轮询选择 的基本处理逻辑。过滤条件使用了AvailabilityPredicate
public class AvailabilityPredicate extends AbstractServerPredicate {
...
@Override
public boolean apply(@Nullable PredicateKey input) {
LoadBalancerStats stats = getLBStats();
if (stats == null) {
return true;
}
return !shouldSkipServer(stats.getSingleServerStat(input.getServer()));
}
// 主要过滤逻辑位于以下方法,主要判断服务实例的两项内容:
// 是否故障,即断路器是否生效已断开
// 实例的并发请求数大于阈值。默认2e32 - 1,该配置可通过参数<clientName>.<nameSpace>.ActiveConnectionLimit来修改
// 以上两项有一点满足apply 就返回false (代表该节点存在故障或负载过高)
private boolean shouldSkipServer(ServerStats stats) {
if ((CIRCUIT_BREAKER_FILTERING.get() && stats.isCircuitBreakerTripped())
|| stats.getActiveRequestsCount() >= activeConnectionsLimit.get()) {
return true;
}
return false;
}
}
该策略调用父类实现的choose作为备选策略(先遍历所有的节点进行过滤,然后在过滤后的集合中选择实例),本身以线性的方式选择一个实例,接着用过滤条件来判断该实例是否满足要求,重复10次没有找到符合要求的实例就采用父类的实现方案。
public class AvailabilityFilteringRule extends PredicateBasedRule {
private AbstractServerPredicate predicate;
...
/**
* This method is overridden to provide a more efficient implementation which does not iterate through
* all servers. This is under the assumption that in most cases, there are more available instances
* than not.
*/
@Override
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);
}
}
总结:该策略通过线性抽样的方式直接尝试寻找可用且较空闲的实例来使用,优化父类每次都要遍历所有实例的开销。
ZoneAvoidanceRule
PredicateBasedRule的具体实现类。ZoneAvoidanceRule中有用于选择Zone区域策略的一些静态函数,createSnapshot,getAvailableZones。
下面是ZoneAvoidanceRule作为服务实例过滤条件的实现原理:它使用了CompositePredicate来进行服务实例清单的过滤。这是一个组合过滤条件,在其构造函数中,它以ZoneAvoidancePredicate为主过滤条件,AvailabilityPredicate为次过滤条件初始化了组合过滤条件的实例。
public class ZoneAvoidanceRule extends PredicateBasedRule {
private static final Random random = new Random();
private CompositePredicate compositePredicate;
public ZoneAvoidanceRule() {
super();
ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this);
AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this);
compositePredicate = createCompositePredicate(zonePredicate, availabilityPredicate);
}
...
}
ZoneAvoidanceRule在实现的时候并没有像AvailabilityFilteringRule那样重写choose函数来优化,所以它完全遵循了父类的过滤主逻辑:“先过滤清单,再轮询选择”。其中过滤清单的条件就是CompositePredicate。
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;
/**
* Get the filtered servers from primary predicate, and if the number of the filtered servers
* are not enough, trying the fallback predicates
*/
@Override
public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) {
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中,它的处理逻辑如下所示:
- 使用过滤条件对所有实例过滤并返回过滤后的实例清单。
- 依次使用次过滤条件列表中的过滤条件对主过滤条件的结果进行过滤。
- 每次过滤之后(包括主次过滤条件),都需要判断下面的条件,只要有一个符合就不再过滤,将当前结果返回供线性轮询算法选择:
-
- 过滤后的实例总数 >= 最小过滤实例数(minimalFilteredServers,默认1)。
-
- 过滤后的实例比例 > 最小过滤百分比(minimalFilteredPercentage,默认0).
配置详解
自动化配置
引入Spring Cloud Ribbon后,自动化构建以下接口的实现:
- IClientConfig:Ribbon的客户端配置,默认采用com.netflix.client.config.DefaultClientConfigImpl实现
- IRule:Ribbon的负载均衡策略,默认采用com.netflix.loadbalancer.ZoneAvoidanceRule实现,该策略能够在多区域环境下选出最佳区域的实例进行访问
- IPing:Ribbon的实例检查策略,默认采用com.netflix.loadbalancer.NoOpPing实现,该检查策略是一个特殊的实现,实际上它并不会检查实例是否可用,而是始终返回true,默认认为所有服务实例都是可用的
- ServerList:服务实例清单的维护机制,默认采用com.netflix.loadbalancer.ConfigurationBasedServerList实现
- ServerListFilter:服务实例清单过滤机制,默认采用org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter实现,该策略能够优先过滤出与请求调用方处于同区域的服务实例。
- ILoadBalancer:负载均衡器,默认采用com.netflix.loadbalancer.ZoneAwareLoadBalancer实现,它具备区域感知的能力
以上自动化配置仅在没有引入Spring Cloud Eureka等服务治理框架时如此,在同时引入Eureka和Ribbon依赖时会有所不同。
个性化配置
需要在Spring Boot应用中创建对应的实现实例就能覆盖这些默认的配置实现。如以下内容
@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 RibbonClientfiguration() {}
注意:
在Camden版本之上通过配置定义个性化配置:主要是PropertiesFactory类动态创建接口的实现
<clientName>.ribbon.<key>=<value>
实现将hello-service服务客户端的IPing接口实现替换为PingUrl,只需以下方式
hello-service.ribbon.NFLoadBalancerPingClassName=com.netflix.loadbalancer.PingUrl
其中hello-service是服务名,NFLoadBalancerPingClassName参数用来指定具体的IPing接口实现类。
除了NFLoadBalancerPingClassName参数之外还提供了其他几个接口的动态配置实现:
NFLoadBalancerClassName:配置ILoadBalancer接口的实现
NFLoadBalancerPingClassName:配置IPing接口的实现
NFLoadBalancerRuleClassName:配置IRule接口的实现
NIWSServerListClassName:配置ServerLIst接口的实现
NIWSServerListFilterClassName:配置ServerListFilter接口的实现
通过配置指定RibbonClient的ILoadBalancer,IPing,IRule,ServerLIst,ServerListFilter的定制化实现。
参数配置
Ribbon的参数配置通常有两种方式:全局配置和指定客户端配置
全局配置:
使用ribbon.=格式进行配置即可。 代表了Ribbon客户端 配置的参数名,代表对应参数的值。如下
ribbon.ConnectTimeout=250
全局配置可以作为默认值进行设置,当指定客户端配置了相应key的值时,将覆盖全局配置的内容。
指定客户端的配置:
使用.ribbon.=的格式进行配置。代表客户端的名称,可以理解为服务名。
若没有服务治理框架的帮助,需要手动为客户端指定具体的实例清单,如下
hello-service.ribbon.listOfServers=localhost:8001,localhost:8002
对于Ribbon参数的key以及value类型的定义,更多的需要看com.netflix.client.config.CommonClientConfigKey类获取
与Eureka结合
同时引入Eureka和Ribbon会触发Eureka中实现的对Ribbon的自动化配置
ServerList的维护机制实现被com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList的实例所覆盖,该实现会将服务清单列表交给Eureka的服务治理机制来维护;
IPing的实现将被com.netflix.niws.loadbalancer.NIWSDiscoveryPing的实例所覆盖,该实现也将实例检查的任务交给了服务治理框架来进行维护;
默认情况,用于获取实例请求的ServerList接口实现将采用Spring Cloud Eureka中封装的org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList,其目的是为了让实例维护策略更通用,所以将使用物理元数据进行负载均衡,而不是使用原生的AWS AMI元数据。
此时对于Ribbon的参数配置依然可以采用之前的两种配置方式,指定客户端的配置方式可直接使用Eureka中的服务名作为<client>来完成针对各个微服务的个性化配置
此外由于Spring Cloud Ribbon默认实现了区域亲和策略,所以,可通过Eureka实例的元数据配置来实现区域化的实例配置方案。比如,可将处于不同机房的实例配置成不同的区域值,以作为跨区域的容错机制实现。
以上实现方式只需在服务实例的元数据中增加zone参数来指定自己所在的区域如:
eureka.instance.metadataMap.zone=beijing
可通过参数配置的方式禁用Eureka对Ribbon服务实例的维护实现。
ribbon.eureka.enabled=false
此时服务实例的维护将回归到使用<client>.ribbon.listOfServers参数配置的方式来实现了
重试机制
Spring Cloud Eureka实现的服务治理机制强调CAP原理中的AP,即可用性与可靠性,它与ZooKeeper之类强调CP(一致性,可靠性)的服务治理框架最大的区别就是,Eureka为了实现更高的服务可用性,牺牲了一定的一致性,极端情况下宁愿接受故障实例也不丢掉"健康"实例,如Eureka有心跳保护机制,注册中心保留触发此机制此时的所有节点,保证服务间互相调用的场景,而AP的ZooKeeper会剔除这些实例。
Camden SR2后的版本,Spring Cloud整合了Spring Retry增加RestTemplate的重试能力,仅需配置
以hello-service服务为例
// 该参数用来开启重试机制,默认关闭
spring.cloud.loadbalancer.retry.enabled=true
// 改参数源码定义
@ConfigurationProperties("spring.cloud.loadbalancer.retry")
public class LoadBalancerRetryProperties {
private boolean enabled = false;
...
}
// 断路器的超时时间需要大于Ribbon的超时时间,不然不会触发重试
hystrix.command.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
根据以上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由MaxAutoRetries配置),若不行,就换一个实例进行访问,若还不行,再换实例访问,更换次数由MaxAutoRetriesNextServer配置,依然不行返回失败信息