目录
1. Ribbon简介
负载均衡是指将负载分摊到多个执行单元上,常见的负载均衡有两种方式,第一种是独立进程单元,通过负载均衡策略,将请求转发到不同的执行单元,例如Nginx;另一种是将负载均衡逻辑以代码的形式封装到服务消费者的客户端,服务消费者客户端维护了一份服务器提供者的消息列表,有了消息列表,通过负载均衡策略把请求分摊给多个服务提供者,从而达到负载均衡的目的。
2. Ribbon实例
基于上一篇Eureka的基础上创建一个eureka-ribbon-client的project
引入pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>eureka-ribbon-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka-ribbon-client</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml书写相关配置
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
port: 8764
spring:
application:
name: eureka-ribbon-client
程序入口打上EnableEurekaClient注解开启EurekaClient的功能
@EnableEurekaClient
@SpringBootApplication
public class EurekaRibbonClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaRibbonClientApplication.class, args);
}
}
我们书写一个RestFul APi接口,在API接口内部需要调用eureka-client的API接口hello即服务消费。
eureka-client我们之前搞了两个实例8762和8763,我们要做的就是调用ribbon接口hi的时候可以轮流访问8762和8763的两个实例。
首先我们需要在程序的IOC容器中注入restTemplate的Bean并且打上@LoadBalanced注解,这样restTemplate就开启了负载均衡的功能
@Configuration
public class RibbonConfig {
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
}
然后写RibbonService类用来调用eureka-client的APi接口
@Service
public class RibbonService {
@Autowired
RestTemplate restTemplate;
public String hello(String name) {
return restTemplate.getForObject("http://eureka-client/hello?name=" + name, String.class);
}
}
然后写controller
@RestController
public class RibbbonController {
@Autowired
RibbonService ribbonService;
@RequestMapping("/hi")
public String hi(@RequestParam String name) {
return ribbonService.hello(name);
}
}
这样我们就是实现了ribbon的简单实例,我们调用hi的时候就会随机去访问eureka-client的两个实例。
用如下一张图来显示就是
当我们配置了负载均衡客户端,对服务器发送请求的时候,会根据目前eureka server中还活跃的服务,结合自己配置中的ribbon对应的负载均衡算法进行负载请求。
通过如下两部即可:
1)服务提供者只需要启动多个服务实例并且注册到一个注册中心或者是多个相关联的服务注册中心
2)服务消费者直接通过调用被@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用
这样我们就可以将服务提供者的高可用以及服务消费者的负载均衡调用一起实现了。
2.1 参数配置
对于Ribbon的参数 配置通常有两种方式: 全局配置以及指定客户端配置。
全局配置的方式很简单, 只需使用 ribbon. < key>= < value>格式进行配置即可。比如, 我们可以像下面这样全局配置Ribbon创建连接的超时时间:
ribbon.ConnectTimeout = 250
指定客户端的配置方式 采用< client> .ribbon. < key>= < value>的格式进行配置。< client>代表了客户端的名称, 如上文中我们在@RibbonClient中指定的名称,也可以将它理解 为是一个服务名。
cloud-provider.ribbon.listOfServers = localhost:8001,localhost:8002, localhost:8003
对于Ribbon 参数的key以及value类型的定义,可以通过查看com.netflix.client.config.CommonClientConfigKey类获得更为详细的配置 内容, 不进行详细介绍。
2.2 重试机制
由于SpringCloud Eureka实现的服务治理机制强调了CAP原理中的AP, 即可用性与可靠性,它与Zoo Keeper这类强调CP(一 致性、可靠性)的服务治理框架最大的区别就是,Eureka为了实现更高的服务可用性, 牺牲了 一 定的 一 致性, 在极端情况下它宁愿接受故障实例也不要丢掉“健康”实例, 比如, 当服务注册中心的网络发生故障断开时, 由于所有的服务实例无法维持续约心跳, 在强调AP的服务治理中将会把所有服务实例都剔除掉,而Eureka则会因为超过85%的实例丢失心跳而会触发保护机制,注册中心将会保留此时的所有节点, 以实现服务间依然可以进行互相调用的场景, 即使其中有部分故障节点, 但这样做可以继续保障大多数的服务正常消费。
由于SpringCloud Eureka在可用性与 一 致性上的取舍, 不论是由于触发了保护机制还是服务剔除的延迟, 引起服务调用到故障实例的时候, 我们还是希望能够增强对这类问题的容错。 所以, 我们在实现服务调用的时候通常会加入 一 些重试机制。 从CamdenSR2版本开始,SpringCloud整合了SpringRetry来增强RestTernplate的重试能力, 对于开发者来说只需通过简单的配置, 原来那些通过RestTemplate 实现的服务访问就会自动根据配置来实现重试策略。
以我们上文服务的调用为例, 可以在配置文件中增加如下内容:
spring.cloud.loadbalancer.retry.enabled=true//该参数用来开启重试机制
hystrix.command.default.execution.isolation.thread.timeoutinMilliseconds = 10000 //断路器的超时时间需要大于Ribbon的超时时间, 不然不会触发重试。
cloud-provider.ribbon.ConnectTimeout = 250 //请求连接的超时时间。
cloud-provider.ribbon.ReadTimeout = 1000 //请求处理的超时时间。
cloud-provider.ribbon.OkToRetryOnAllOperations = true //对所有操作请求都进行重试。
cloud-provider.ribbon.MaxAutoRetriesNextServer = 2 //切换实例的重试次数。
cloud-provider.ribbon.MaxAutoRetries = 1 //对当前实例的重试次数。
根据如上配置, 当访问到故障请求的时候, 它会再尝试访问一 次当前实例(次数由MaxAutoRetries配置), 如果不行, 就换一 个实例进行访问, 如果还是不行, 再换 一 次实例访问(更换次数由MaxAutoRetriesNextServer配置), 如果依然不行, 返回失败信息。
3. Ribbon源码解析
为了深入理解Ribbon,我们就从源码的角度来理解下Ribbon,看他如何与eureka结合,并且如何和RestTemplate结合来做负载均衡。
我们回顾下消费者的实例代码
其中@LoadBalanced这个注解对我们来说是比较陌生的,所以从这个角度来切入开启ribbon的源码之旅
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}
这个注解是提供给restTemplate做标记,以使用负载均衡的客户端LoadBalancerClient来配置它,所以我们进入LoadBalancerClient
LoadBalancerClient 注解
通过以下LoadBalancerClient的源码可以看出,它是springCloud的一个接口
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;
URI reconstructURI(ServiceInstance instance, URI original);
}
它还继承了一个接口ServiceInstanceChooser,我们看下
public interface ServiceInstanceChooser {
ServiceInstance choose(String serviceId);
}
我们来看下方法功能:
choose:根据传入的服务名serviceID,从负载均衡器中挑选一个对应服务的实例
execute:使用负载均衡器中挑选出来的服务实例来执行请求内容
reconstructURI:为系统构建一个合适的host:port形式的url
顺着 LoadBalancerClient 接口的所属包 org.springframework.cloud.client.loadbalancer, 我们对其内容进行整理, 可以得出如下图所示的关系。
LoadBalancerAutoConfiguration 自动装配类
根据类名,我们就可以大致猜测这是为实现客户端负载均衡器的自动化配置类。
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({RestTemplate.class})
@ConditionalOnBean({LoadBalancerClient.class})
@EnableConfigurationProperties({LoadBalancerRetryProperties.class})
public class LoadBalancerAutoConfiguration {
//省略代码...
}
从类的注解就可以看出Ribbon的自动注解需要满足如下两个条件
1)@ConditionalOnClass({RestTemplate.class})
RestTemplate必须要存在于当前工程的环境中
2)@ConditionalOnBean({LoadBalancerClient.class})
当前spring工程中必须有LoadBalancerClient的实现类
在LoadBalancerAutoConfiguration中主要做了如下三件事:
1)创建了 一 个LoadBalancerInterceptor的Bean, 用以实现对客户端发起请求时进行拦截, 以实现客户端负载均衡
2)创建了一 个RestTemplateCustomizer的Bean,用于给RestTemplate增加LoadBalancerInterceptor拦截器。
3)维护了一 个被@LoadBalanced 注解修饰的RestTemplate对象列表,并在这里进行初始化, 通过调用RestTemplateCustomizer的实例来给需要客户端负载均衡的RestTemplate增加LoadBalancerinterceptor拦截器。
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) {
this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
}
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final 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 函数去根据服务名来选择实例并发起实际的请求。分析到这里, LoadBalancerClien 七还只是 一 个抽象的负载均衡器接口, 所以我们还需要找到它的具体实现类来进 一 步进行分析。 通过查看Ribbon的源码, 可以很容易地在org.springframework.cloud.netflix.ribbon 包下找到对应的实现类 RibbonLoadBalancerClient:我们着重看 excute方法
RibbonLoadBalancerClient #execute方法
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 {
T returnVal = request.apply(serviceInstance);
statsRecorder.recordStats(returnVal);
return returnVal;
} catch (IOException var8) {
statsRecorder.recordStats(var8);
throw var8;
} catch (Exception var9) {
statsRecorder.recordStats(var9);
ReflectionUtils.rethrowRuntimeException(var9);
return null;
}
}
}
RibbonLoadBalancerClient的execute方法实现,首先是通过getServer根据传入的服务名serviceID来获取具体的服务实例
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
return loadBalancer == null ? null : loadBalancer.chooseServer(hint != null ? hint : "default");
}
我们在这边可以看到获取具体服务实例没有使用LoadBalancerClient接口中的choose方法,而是ILoadBalancer接口中定义的chooseServer函数
我们来看看这个接口
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();
}
在该接口定义中涉及的 Server 对象定义是 一 个传统的服务端节点, 在该类中存储了服务端节点的 一 些元数据信息, 包括 host、 port 以及 一 些部署信息等。而对于该接口的实现,我们整理出如下图所示的结构。可以看到, BaseLoadBalancer 类实现了基础的负载均衡器,而 DynarnicServerListLoaclBalancer 和 ZoneAwareLoaclBalancer在负载均衡的策略上做了 一 些功能的扩展。
那么在整合Ribbon 的时候 Spring Cloud 默认采用了哪个具体实现呢?我们通过RibbonClientConfiguration 配置类, 可以知道在整合时默认采用了 ZoneAwareLoadBalancer 来实现负载均衡器。
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server> serverList, ServerListFilter<Server> serverListFilter, IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
return (ILoadBalancer)(this.propertiesFactory.isSet(ILoadBalancer.class, this.name) ? (ILoadBalancer)this.propertiesFactory.get(ILoadBalancer.class, config, this.name) : new ZoneAwareLoadBalancer(config, rule, ping, serverList, serverListFilter, serverListUpdater));
}
下面, 我们再回到 RibbonLoadBalancerClient 的 execute 函数逻辑, 在通过ZoneAwareLoadBalancer 的 chooseServer 函数获取了负载均衡策略分配到的服务实例对象 Server 之后, 将其内容包装成RibbonServer 对象(该对象除了存储了服务实例的信息之外, 还增加了服务名 serviceId、 是否需要使用 HTTPS 等其他信息), 然后使用该对象再回调 LoadBalancerinterceptor 请求拦截器中 LoadBalancerRequest的 apply(final Serviceinstance instance) 函数, 向一 个实际的具体服务实例发起请求,从而实现 一 开始以服务名为 host 的 URI 请求到 host:post 形式的实际访问地址的转换。在 apply(final Serviceinstance instance) 函数中传入的 Serviceinstance接口对象是对服务实例的抽象定义。 在该接口中暴露了服务治理系统中每个服务实例需要提供的 一 些基本信息, 比如 serviceld 、 host 、 port 等, 具体定义如下:
public interface ServiceInstance {
default String getInstanceId() {
return null;
}
String getServiceId();
String getHost();
int getPort();
boolean isSecure();
URI getUri();
Map<String, String> getMetadata();
default String getScheme() {
return null;
}
}
而上面提到的具体包装 Server 服 务 实 例 的 RibbonServer 对 象就是Service Instance 接口的实现, 可以看到它除了包含 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.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;
}
//省略代码...
}
那么 apply (final Serviceinstance instance) 函数在接收到了具体Serviceinstance 实例后,是如何通过 LoadBalancerClient 接口中的 reconstructUR 工操作来组织具体请求地址的呢?
public ListenableFuture<ClientHttpResponse> apply(final ServiceInstance instance) throws Exception {
HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, AsyncLoadBalancerInterceptor.this.loadBalancer);
return execution.executeAsync(serviceRequest, body);
}
从 apply 的实现中, 可以看到它具体执行的时候, 还传入了 ServiceRequestWrapper 对象, 该对象继承了 HttpRequestWrapper 并重写了 getURI 函数, 重写后的 getUR 工通过调用 LoadBalancerClient 接口的 reconstructURI 函数来重新构建一个 URI 来进行访问。
public URI getURI() {
URI uri = this.loadBalancer.reconstructURI(this.instance, this.getRequest().getURI());
return uri;
}
在 LoadBalancerinterceptor 拦截器中,AsyncRequestExecution 的实例具体执行 execution.executeAsync(serviceRequest, body) 时, 会调用InterceptingAsyncClientHttpRequest下 AsyncRequestExecution类的 executeAysnc 函数,具体实现如下
public ListenableFuture<ClientHttpResponse> executeAsync(HttpRequest request, byte[] body) throws IOException {
if (this.iterator.hasNext()) {
AsyncClientHttpRequestInterceptor interceptor = (AsyncClientHttpRequestInterceptor)this.iterator.next();
return interceptor.intercept(request, body, this);
} else {
URI uri = request.getURI();
HttpMethod method = request.getMethod();
HttpHeaders headers = request.getHeaders();
Assert.state(method != null, "No standard HTTP method");
AsyncClientHttpRequest delegate = InterceptingAsyncClientHttpRequest.this.requestFactory.createAsyncRequest(uri, method);
delegate.getHeaders().putAll(headers);
if (body.length > 0) {
StreamUtils.copy(body, delegate.getBody());
}
return delegate.executeAsync();
}
}
可以看到,在创建请求的时候 requestFactory.createAsyncRequest(uri, method) , 这里的 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);
URI uri;
Server server;
if (instance instanceof RibbonLoadBalancerClient.RibbonServer) {
RibbonLoadBalancerClient.RibbonServer ribbonServer = (RibbonLoadBalancerClient.RibbonServer)instance;
server = ribbonServer.getServer();
uri = RibbonUtils.updateToSecureConnectionIfNeeded(original, ribbonServer);
} else {
server = new Server(instance.getScheme(), instance.getHost(), instance.getPort());
IClientConfig clientConfig = this.clientFactory.getClientConfig(serviceId);
ServerIntrospector serverIntrospector = this.serverIntrospector(serviceId);
uri = RibbonUtils.updateToSecureConnectionIfNeeded(original, clientConfig, serverIntrospector, server);
}
return context.reconstructURIWithServer(server, uri);
}
简单介绍一 下上面提到的SpringClientFactory 和 RibbonLoadBalancerContext:
SpringClientFactory 类是 一 个用来创建客户端负载均衡器的工厂类, 该工厂类会为每 一 个不同名的 Ribbon 客户端生成不同的 Spring 上下文。
RibbonLoadBalancerContext 类是 LoadBalancerContext的子类, 该类用于存储一些被负载均衡器 使用的上下文内容和 API操作 reconstructURIWithServer 就是其中之 一 )。
从 reconstructURIWithServer 的实现中我们可以看到,它同 reconstructURI的定义类似。 只是 reconstructURI 的第 一 个保存具体服务实例的参数使用了 SpringCloud 定义的 ServiceInstance, 而reconstructURLWithServer 中使用了 Netflix中定义的 Server, 所以在 RibbonLoadBalancerClient 实现 reconstructURI 的时候, 做了一 次转换, 使用Serviceinstance 的 host 和 poot 信息构建了一 个Server对象来给 reconstructURIWithServer 使用。 从 reconstructURIWithServer 的实现逻辑中, 我们可以看到, 它从 Server 对象中获取 host 和 port信息, 然后根据以服务名为 host 的 URI 对象 original 中获取其他请求信息, 将两者内容进行拼接整合, 形成最终要访间的服务实例的具体地址。
分析 到这里, 我们已经可以大致理清Spring Cloud R巾bon中实现客户端负载均衡的基本脉络,了解了它是如何通过 LoadBalancerinterceptor拦截器对 RestTemplate的请求进行拦截, 并利用Spring Cloud的负载均衡器 LoadBalancerClient将以逻辑服务名为host的 URI转换成具 体 的 服 务 实 例地址的过程。 同 时 通 过分 析LoadBalancerClient的Ribbon实现RibbonLoadBalancerClient, 可以知道在使用Ribbon实现负载均衡器的时候,实际使用的还是Ribbon中定义的ILoadBalancer接口的实现,自动化配置会采用ZoneAwareLoadBalancer的实例来实现客户端负载均衡。