写在前面
该文参考来自 程序猿DD 的Spring Cloud 微服务实战一书,该文是作为阅读了 spring cloud ribbon一章的读书笔记。书中版本比较老,我选择了最新稳定版的 spring cloud Greenwich.SR2 版本,该版本较书中版本有些变动。非常感谢作者提供了这么好的学习思路,谢谢!
1. 介绍
Spring Cloud Ribbon
基于 Netflix Ribbon 实现。它是一个基于 HTTP 和 TCP 的客户端负载均衡工具。通过 Spring Cloud 的封装,可以让我们非常容易的将面向服务的 REST 模板请求自动转换成客户端负载均衡的服务调用(基本上属于无感的)。
Spring Cloud Ribbon
虽然只是一个工具类框架,它不像服务注册中心、配置中心、API 网关那样需要独立部署,但是 它几乎存在于每一个 Spring Cloud 构建的微服务和基础设施中。因为微服务间的调用,API 网关的请求转发等内容,实际上都是通过 Ribbon 来实现的。
2. 简单使用
引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
直接通过调用被 @LoadBalanced
注解修饰过的 RestTemplate
来实现面向服务的接口调用。这样就可以将服务提供者的高可用以及服务消费者的负载均衡调用一起实现了。
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
那么,它是如何实现的呢?先来看看插入点,通过在执行真正的请求之前,为 restTemplate 设置拦截器,重新构造 URI 。
LoadBalancerAutoConfiguration.java 配置类通过如下代码做了这一步:
@Bean
@ConditionalOnMissingBean
public LoadBalancerRequestFactory loadBalancerRequestFactory(
LoadBalancerClient loadBalancerClient) {
return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
}
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
}
查看 LoadBalancerInterceptor 源码的拦截方法:
@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,
this.requestFactory.createRequest(request, body, execution));
}
可以发现最终的执行会调用 org.springframework.cloud.client.loadbalancer.LoadBalancerClient
接口的 execute 方法。查看该接口的实现,我们可以发现一个 RibbonLoadBalancerClient
实现类。该类的注入是在 RibbonAutoConfiguration
配置类中,查看 execute 方法的具体实现:
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
throws IOException {
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer, hint);
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);
}
@Override
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;
}
不难发现,首先选择 service 对应的负载均衡器,根据负载均衡器选择对应的 server 。最后,接口请求执行是由 LoadBalancerRequest
执行的,查看该接口的实现:
// LoadBalancerRequestFactory.java
public LoadBalancerRequest<ClientHttpResponse> createRequest(
final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) {
return instance -> {
HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance,
this.loadBalancer);
if (this.transformers != null) {
for (LoadBalancerRequestTransformer transformer : this.transformers) {
serviceRequest = transformer.transformRequest(serviceRequest,
instance);
}
}
return execution.execute(serviceRequest, body);
};
}
最后 ,ClientHttpRequestExecution 的 execute 方法接受的第一个参数变为了 ServiceRequestWrapper 类,查看该类,发现该类重写了 getURI 方法:
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;
}
}
不过最后的 URI 还是 调用 LoadBalancerClient(reconstructURI方法) 来重构的,整个流程就是这样了。选取 server 是通过负载均衡器和负载均衡策略来完成的,通过分析负载均衡器 ILoadBalancer
接口的实现类可以发现,最后 server 是根据负载均衡策略 IRule
接口的实现类来选取的。
在我看来,负载均衡器更多的是维护了可用的服务清单列表,当然还有其它的状态信息,这样实现了服务提供者的高可用。负载均衡器策略则是根据根据可用的服务清单列表选取服务,这实现了服务消费者的负载均衡,下面将介绍这些实现类。
3. 负载均衡器
Spring Cloud 中定义了 LoadBalancerClient
作为负载均衡器的通用接口,并且针对 Ribbon 实现了 RibbonLoadBalancerClient
,但是它在具体实现客户端负载均衡时,是通过 Ribbon 的 ILoadBalancer 接口实现的。通过上面的 LoadBalancerClient
源码也可以看出:
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer, hint);
ILoadBalancer 接口的继承关系图如下:
其中 BaseLoadBalancer 类是 Ribbon 负载均衡器的基础实现类,类中定义了很多关于负载均衡器相关的基础内容。
DynamicServerListLoadBalancer
类,实现了服务实例清单在运行期的动态更新能力,这指的是能够动态的从注册中心获取服务列表;同时,它还具备了对服务实例清单的过滤功能,这指的是,我们可以通过过滤器来选择性地获取一批服务实例清单。
ZoneAwareLoadBalancer
负载均衡器是对 DynamicServerListLoadBalancer
的拓展,它实现了区域亲和特性,在确定了某个 Zone 区域后,则获取了对应 Zone 区域的负载均衡器。
@Override
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);
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);
}
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);
}
}
4. 负载均衡策略
用于负载均衡器中的服务实例选择。其实现类如下:
RandomRule
实现从服务实例清单中随机选择一个服务实例的功能。RoundRobinRule
实现了按照线性轮询的方式依次选择每个服务实例的功能。RetryRule
实现了一个具备重试机制的实例选择功能。WeightedResponseTimeRule
是对RoundRobinRule
的拓展,增加了根据实例的运行情况来计算权重,并根据权重来挑选实例。ClientConfigEnableRoundRobinRule
通过继承该策略,在子类中做一些高级策略时有可能会存在一些无法实施的情况,那么就可以用父类的实现作为备选(线性轮询机制)。BestAvailableRule
通过遍历负载均衡器中维护的所有服务实例,会过滤掉故障的实例,并找出并发请求数最小的一个,所以该策略的特性是可选出最空闲的实例。PredicateBasedRule
先通过子类实现中的 Predicate 逻辑来过滤一部分服务实例,然后再以线性轮询的方式从过滤后的实例清单中选出一个。AvailabilityFilteringRule
通过线性抽样的方式直接尝试寻找可用且较空闲的实例来使用。ZoneAvoidanceRule
负载均衡策略有很多苛刻的条件,如果出现了一些很奇怪的现象的话,建议查看源码。我在上面的描述也是非常片面,因为我无法摘抄书上的全部内容,这里只是做一个笔记作用,具体的还是看源码吧。