关注“Java艺术”一起来充电吧!
在《OpenFeign
与Ribbon
源码分析总结》这篇文章中,我们只是简单地了解Ribbon
的重试机制的实现原理,本篇我们再对Ribbon
的重试机制的实现做详细分析,从源码分析找出我们想要的答案,即如何配置Ribbon
实现调用每个服务使用不一样的重试策略,如配置失败重试多少次,以及使用自定义重试策略RetryHandler
。
Ribbon
重试机制地实现源码分析
Ribbon
的重试策略配置
如何替换RetryHandler
?
Feign
会为每个服务提供者创建一个Client发起请求,Client即服务消费端。我们把每个服务分为服务提供端Server
和服务消费端Client。
LoadBalancerFeignClient
:OpenFeign
整合Ribbon
时使用的Client
(OpenFeign
使用Client
发送请求);
FeignLoadBalancer
:OpenFeign
整合Ribbon
的桥接器,由LoadBalancerFeignClient
创建;
LoadBalancerCommand
:Ribbon
将请求转为RxJava API
调用的实现,由FeignLoadBalancer
调用;
CachingSpringLoadBalancerFactory
:OpenFeign
整合Ribbon
用于创建FeignLoadBalancer
桥接器的带缓存功能的FeignLoadBalancer
工厂。
RibbonLoadBalancerClient
:Ribbon
提供的实现Spring Cloud
负载均衡接口(LoadBalancerClient
)的类;
RibbonAutoConfiguration
:Ribbon
的自动配置类,注册RibbonLoadBalancerClient
到Spring
容器。
SpringClientFactory
:Ribbon
用于自己管理ApplicationContext
,Ribbon
会为每个Client创建一个ApplicationContext
;
RibbonClientConfiguration
:Ribbon
为每个Client提供ApplicationContext
实现环境隔离,这是Ribbon
为每个Client创建ApplicationContext
时都使用的配置类,用于注册Ribbon
的各种功能组件,如负载均衡器ILoadBalancer
;
RequestSpecificRetryHandler
:RetryHandler
接口的实现类,OpenFeign
整合Ribbon
使用的默认失败重试策略处理器;
Ribbon
重试机制地实现源码分析Ribbon
的重试机制使用了RxJava
的API
,而重试次数以及是否重试的决策由RetryHandler
实现。Ribbon
提供两个RetryHandler
的实现类,如下图所示。
现在我们要找出Ribbon
使用的是哪个RetryHandler
。我们只分析OpenFeign
与Ribbon
整合的使用,Spring Cloud
的@LoadBalanced
注解方式使用我们不做分析。
spring-cloud-netflix-ribbon
的spring.factories
文件导入的自动配置类是RibbonAutoConfiguration
,该配置类向Spring
容器注入了一个RibbonLoadBalancerClient
。RibbonLoadBalancerClient
正是Ribbon
为Spring Cloud
的负载均衡接口提供的实现类。
在创建RibbonLoadBalancerClient
时给构造方法传入了一个SpringClientFactory
,源码如下。
@Configuration
public class RibbonAutoConfiguration{
// 创建RibbonLoadBalancerClient
@Bean
@ConditionalOnMissingBean(LoadBalancerClient.class)
public LoadBalancerClient loadBalancerClient() {
return new RibbonLoadBalancerClient(springClientFactory());
}
}
SpringClientFactory
是Ribbon
使用的ApplicationContext
,Ribbon
会为每个Client都创建一个AnnotationConfigApplicationContext
,用作环境隔离。SpringClientFactory与每个Client的AnnotationConfigApplicationContext的关系如下图所示。
SpringClientFactory
在调用父类构造方法时传入了一个配置类:RibbonClientConfiguration
,源码如下。
public class SpringClientFactory extends NamedContextFactory{
public SpringClientFactory() {
super(RibbonClientConfiguration.class, NAMESPACE, "ribbon.client.name");
}
}
RibbonClientConfiguration
配置类在每个Client对应的AnnotationConfigApplicationContext
初始化时生效,每个Client的AnnotationConfigApplicationContext都是在第一次调用服务的接口时才被创建。创建ApplicationContext
并且调用register
方法注册RibbonClientConfiguration
配置类以及其它一些配置类,最后调用其refresh
方法初始化该ApplicationContext
。
RibbonClientConfiguration
负责为每个Client对应的ApplicationContext
注入服务列表(ServerList
)、服务列表更新器(ServerListUpdater
)、负载均衡器(ILoadBalancer
)、负载均衡算法(IRule
)、客户端配置(IClientConfig
)、重试决策处理器(RetryHandler
)等。
服务列表ServerList
:从注册中心获取可用服务提供者节点;
服务列表更新器ServerListUpdater
:定时更新本地缓存的服务列表,调用ServerList
从注册中心获取;
负载均衡算法IRule
:实现各种负载均衡算法,如随机、轮询等;
负载均衡器ILoadBalancer
:调用负载均衡算法IRule
选择一个服务提供者节点调用;
重试决策处理器RetryHandler
:决定本次失败是否重试;
由于RibbonClientConfiguration
注册的Bean
是注册在Client隔离的ApplicationContext
中的, 所以调用每个服务提供者的接口将可以使用不同的客户端配置(IClientConfig
)、重试决策处理器(RetryHandler
)等。这也是我们能够为Ribbon
配置调用每个服务的接口使用不一样的重试策略的前提条件,不过这也不是充分必要条件。
RibbonClientConfiguration
配置类会注册一个重试决策处理器RetryHandler
,但这个RetryHandler
并未被使用,也可能是别的地方使用。
@Configuration
public class RibbonClientConfiguration{
// 未使用
@Bean
@ConditionalOnMissingBean
public RetryHandler retryHandler(IClientConfig config) {
return new DefaultLoadBalancerRetryHandler(config);
}
}
OpenFeign
整合Ribbon
时,真正使用的RetryHandler
是RequestSpecificRetryHandler
。前面我们分析OpenFeign
整合Ribbon
源码时提到一个启到桥接作用的类:FeignLoadBalancer
。
当OpenFeign
整合Ribbon
使用时,OpenFeigin
使用的Client
是LoadBalancerFeignClient
,由LoadBalancerFeignClient
创建FeignLoadBalancer
,并调用FeignLoadBalancer
的executeWithLoadBalancer
方法实现负载均衡调用接口。
executeWithLoadBalancer
方法实际是FeignLoadBalancer
的父类AbstractLoadBalancerAwareClient
提供的方法,其源码如下(有删减)。
public abstract class AbstractLoadBalancerAwareClient{
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
LoadBalancerCommand command = buildLoadBalancerCommand(request, requestConfig);
try {
return command.submit({....})
.toBlocking()
.single();
} catch (Exception e) {
}
}
}
executeWithLoadBalancer
方法中会创建一个LoadBalancerCommand
,然后调用LoadBalancerCommand
的submit
方法提交请求Operation,submit
方法源码如下(有删减):
public Observable submit(final ServerOperation operation) {
// .......
// 获取重试次数
final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer();
final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer();
// Use the load balancer
Observable o = (server == null ? selectServer() : Observable.just(server))
.concatMap(new Func1>() {
@Override
public Observable call(Server server) {
//.......
// 相同节点的重试
if (maxRetrysSame > 0)
o = o.retry(retryPolicy(maxRetrysSame, true));
return o;
}
});
// 不同节点的重试
if (maxRetrysNext > 0 && server == null)
o = o.retry(retryPolicy(maxRetrysNext, false));
return o.onErrorResumeNext(...);
}
submit
方法中调用retryHandler
的getMaxRetriesOnSameServer
方法和getMaxRetriesOnNextServer
方法分别获取配置maxRetrysSame
、maxRetrysNext
。maxRetrysSame
表示调用相同节点的重试次数,默认为0
;maxRetrysNext
表示调用不同节点的重试次数,默认为1
。
retryPolicy
方法返回的是一个包装RetryHandler
重试决策者的RxJava API
的对象,最终由该RetryHandler
决定是否需要重试,如抛出的异常是否允许重试。而是否达到最大重试次数则是在retryPolicy
返回的Func2
中完成,这是RxJava
的API
,retryPolicy
方法的源码如下。
private Func2 retryPolicy(final int maxRetrys, final boolean same) {
return new Func2() {
@Override
public Boolean call(Integer tryCount, Throwable e) {
if (e instanceof AbortExecutionException) {
return false;
}
// 大于最大重试次数
if (tryCount > maxRetrys) {
return false;
}
if (e.getCause() != null && e instanceof RuntimeException) {
e = e.getCause();
}
// 调用RetryHandler判断是否重试
return retryHandler.isRetriableException(e, same);
}
};
}
那么这个retryHandler
是怎么来的呢?
FeignLoadBalancer
的executeWithLoadBalancer
方法中调用buildLoadBalancerCommand
方法构造LoadBalancerCommand
对象时创建的,buildLoadBalancerCommand
方法源码如下。
protected LoadBalancerCommand buildLoadBalancerCommand(final S request, final IClientConfig config) {
// 获取RetryHandler
RequestSpecificRetryHandler handler = getRequestSpecificRetryHandler(request, config);
// 使用Builder构造者模式构造LoadBalancerCommand
LoadBalancerCommand.Builder builder = LoadBalancerCommand.builder()
.withLoadBalancerContext(this)
// 传入RetryHandler
.withRetryHandler(handler)
.withLoadBalancerURI(request.getUri());
return builder.build();
}
从源码中可以看出,Ribbon
使用的RetryHandler
是RequestSpecificRetryHandler
。这里还用到了Builder
构造者模式。
FeignLoadBalancer
的getRequestSpecificRetryHandler
方法源码如下:
@Override
public RequestSpecificRetryHandler getRequestSpecificRetryHandler(
RibbonRequest request, IClientConfig requestConfig) {
//.....
if (!request.toRequest().httpMethod().name().equals("GET")) {
// 调用this.getRetryHandler()方法获取一次RetryHandler
return new RequestSpecificRetryHandler(true, false, this.getRetryHandler(),
requestConfig);
}
else {
// 调用this.getRetryHandler()方法获取一次RetryHandler
return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(),
requestConfig);
}
}
RequestSpecificRetryHandler
的构造方法可以传入一个RetryHandler
,这有点像类加载器ClassLoader
实现的双亲委派模型。比如当RequestSpecificRetryHandler
配置的重试次数为0
时,则会获取父RetryHandler
配置的重试次数。
this.getRetryHandler
方法获取到的又是哪个RetryHandler
?(源码在FeignLoadBalancer
的祖父类LoadBalancerContext
中)
[FeignLoadBalancer的父类的父类LoadBalancerContext]
public class LoadBalancerContext{
protected RetryHandler defaultRetryHandler = new DefaultLoadBalancerRetryHandler();
public final RetryHandler getRetryHandler() {
return defaultRetryHandler;
}
}
[FeignLoadBalancer]
public class FeignLoadBalancer extends
AbstractLoadBalancerAwareClient{
public FeignLoadBalancer(ILoadBalancer lb, IClientConfig clientConfig,
ServerIntrospector serverIntrospector) {
super(lb, clientConfig);
// 使用DefaultLoadBalancerRetryHandler
this.setRetryHandler(RetryHandler.DEFAULT);
this.clientConfig = clientConfig;
// IClientConfig,RibbonClientConfiguration配置类注入的
this.ribbon = RibbonProperties.from(clientConfig);
RibbonProperties ribbon = this.ribbon;
// 从IClientConfig中读取超时参数配置
this.connectTimeout = ribbon.getConnectTimeout();
this.readTimeout = ribbon.getReadTimeout();
this.serverIntrospector = serverIntrospector;
}
}
从FeignLoadBalancer
的构造方法中可以看出,RequestSpecificRetryHandler
的父RetryHandler
是DefaultLoadBalancerRetryHandler
。
RetryHandler
接口的定义如下图所示。
RetryHandler
接口方法说明:
isRetriableException方法
:该异常是否可重试;
isCircuitTrippingException
方法:是否是Circuit
熔断类型异常;
getMaxRetriesOnSameServer
方法:调用同一节点的最大重试次数;
getMaxRetriesOnNextServer
方法:调用不同节点的最大重试次数;
Ribbon
的重试策略配置FeignLoadBalancer
在创建RequestSpecificRetryHandler
时传入了IClientConfig
,这个IClientConfig
是从哪里创建的我们稍会再分析。
RequestSpecificRetryHandler
在构造方法中从这个IClientConfig
中获取调用同服务节点的最大重试次数和调用不同服务节点的最大重试次数,源码如下。
public class RequestSpecificRetryHandler implements RetryHandler {
public RequestSpecificRetryHandler(boolean okToRetryOnConnectErrors,
boolean okToRetryOnAllErrors, RetryHandler baseRetryHandler, @Nullable IClientConfig requestConfig) {
// .....
// 从 IClientConfig中获取两种最大重试次数的配置
if (requestConfig != null) {
if (requestConfig.containsProperty(CommonClientConfigKey.MaxAutoRetries)) {
// 获取同节点调用最大重试次数
this.retrySameServer = (Integer)requestConfig.get(CommonClientConfigKey.MaxAutoRetries);
}
if (requestConfig.containsProperty(CommonClientConfigKey.MaxAutoRetriesNextServer)) {
// 获取不同节点调用最大重试次数
this.retryNextServer = (Integer)requestConfig.get(CommonClientConfigKey.MaxAutoRetriesNextServer);
}
}
}
}
requestConfig
是在LoadBalancerFeignClient
创建FeignLoadBalancer
时,从SpringClientFactory
中获取的,也正是RibbonClientConfiguration
自动配置类注入的。
public FeignLoadBalancer create(String clientName) {
FeignLoadBalancer client = this.cache.get(clientName);
if (client != null) {
return client;
}
// this.factory就是SpringClientFactory
IClientConfig config = this.factory.getClientConfig(clientName);
ILoadBalancer lb = this.factory.getLoadBalancer(clientName);
ServerIntrospector serverIntrospector = this.factory.getInstance(clientName,ServerIntrospector.class);
// 创建FeignLoadBalancer
client = this.loadBalancedRetryFactory != null
? new RetryableFeignLoadBalancer(lb, config, serverIntrospector,this.loadBalancedRetryFactory)
: new FeignLoadBalancer(lb, config, serverIntrospector);
// 缓存FeignLoadBalancer
this.cache.put(clientName, client);
return client;
}
IClientConfig
是在RibbonClientConfiguration
中配置的,其源码如下:
public class RibbonClientConfiguration {
// 默认连接超时
public static final int DEFAULT_CONNECT_TIMEOUT = 1000;
// 默认读超时
public static final int DEFAULT_READ_TIMEOUT = 1000;
// 自动注入,${ribbon.client.name}
@RibbonClientName
private String name;
// 注册IClientConfig实例,使用DefaultClientConfigImpl
@Bean
@ConditionalOnMissingBean
public IClientConfig ribbonClientConfig() {
DefaultClientConfigImpl config = new DefaultClientConfigImpl();
config.loadProperties(this.name);
// 配置连接超时
config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT);
// 配置读超时
config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT);
config.set(CommonClientConfigKey.GZipPayload, DEFAULT_GZIP_PAYLOAD);
return config;
}
}
那么我们要怎么修改配置呢?
如何在application
配置文件中配置Ribbon
的重试次数等参数。
我们可以在RibbonClientConfiguration
这个配置类的ribbonClientConfig
方法下断点调试,如下图所示。
从图中可以看出,配置参数key
的格式为:
<服务提供者的名称(serverId)>::<参数名>=
假设我们配置调用服务sck-demo-provider
的最大同节点重试次数为10
,最大不同节点重试次数为12
,连接超时为15
秒,那么我们需要在application-[环境].yaml
配置文件中添加如下配置。
sck-demo-provider:
ribbon:
MaxAutoRetries: 10
MaxAutoRetriesNextServer: 12
ConnectTimeout: 15000
其中MaxAutoRetries
和MaxAutoRetriesNextServer
都能生效,但是ConnectTimeout
配置是不生效的,原因是RibbonClientConfiguration
中创建DefaultClientConfigImpl
时,先调用loadProperties
方法(传入的name
参数就是服务名称)从配置文件获取配置,再调用set
方法覆盖了三个配置:连接超时配置、读超时配置、是否开启gzip
压缩配置。所以这种方式配置连接超时是不生效的。
代码配置就是我们手动注册IClientConfig
,而不使用RibbonClientConfiguration
自动注册的。
RibbonClientConfiguration
配置类注册IClientConfig
的方法上添加了@ConditionalOnMissingBean
条件注解,正因为如此,我们才可以自己注册IClientConfig
。
但要注意一点,RibbonClientConfiguration
是在Ribbon
为每个Client创建的ApplicationContext
中生效的,所以我们需要创建一个配置类(Configuration
),并将其注册到SpringClientFactory
。这样,在SpringClientFactory
为Client创建ApplicationContext
时,就会将配置类注册到该ApplicationContext
。
@Configuration
public class RibbonConfiguration implements InitializingBean {
@Resource
private SpringClientFactory springClientFactory;
@Override
public void afterPropertiesSet() throws Exception {
List cfgs = new ArrayList<>();
RibbonClientSpecification configuration = new RibbonClientSpecification();
// 针对哪个服务提供者配置
configuration.setName(ProviderConstant.SERVICE_NAME);
// 注册的配置类
configuration.setConfiguration(new Class[]{RibbonClientCfg.class});
cfgs.add(configuration);
springClientFactory.setConfigurations(cfgs);
}
// 指定在RibbonClientConfiguration之后生效
@AutoConfigureBefore(RibbonClientConfiguration.class)
public static class RibbonClientCfg {
@Bean
public IClientConfig ribbonClientConfig() {
DefaultClientConfigImpl config = new DefaultClientConfigImpl();
config.setClientName("随便填,不影响,用不到");
config.set(CommonClientConfigKey.MaxAutoRetries, 1);
config.setProperty(CommonClientConfigKey.MaxAutoRetriesNextServer, 3);
config.set(CommonClientConfigKey.ConnectTimeout, 15000);
config.set(CommonClientConfigKey.ReadTimeout, 15000);
return config;
}
}
}
因为Ribbon
是在第一次调用服务的接口时才会创建ApplicationContext
,所以我们在应用程序的Spring
容器初始化阶段获取SpringClientFactory
并为其添加自定义配置类能够生效。
RibbonClientCfg
是我们写的配置类,将其声明在RibbonClientConfiguration
之前生效,这样RibbonClientConfiguration
就不会向容器中注册IClientConfig
了,因此容器中已经存在我们自己注册的IClientConfig。
RetryHandler
?OpenFeign
整合Ribbon
使用时,默认使用的是FeignLoadBalancer
的getRequestSpecificRetryHandler
方法创建的RequestSpecificRetryHandler
。笔者也看了一圈源码,实在找不到怎么替换RetryHandler
,可能OpenFeign
就是不想给我们替换吧。这种情况我们只能另寻辟径了。
既然使用的是FeignLoadBalancer
的getRequestSpecificRetryHandler
方法返回的RetryHandler
,那么我们是不是可以通过继承FeignLoadBalancer
并重写getRequestSpecificRetryHandler
方法实现替换RetryHandler
呢?答案是可以的。
自定义的FeignLoadBalancer
代码如下:
/**
* 自定义FeignLoadBalancer,替换默认的RequestSpecificRetryHandler
*/
public static class MyFeignLoadBalancer extends FeignLoadBalancer {
public MyFeignLoadBalancer(ILoadBalancer lb, IClientConfig clientConfig, ServerIntrospector serverIntrospector) {
super(lb, clientConfig, serverIntrospector);
}
@Override
public RequestSpecificRetryHandler getRequestSpecificRetryHandler(RibbonRequest request, IClientConfig requestConfig) {
// 返回自定义的RequestSpecificRetryHandler
// 参数一:是否连接异常重试时重试
// 参数二:是否所有异常都重试
return new RequestSpecificRetryHandler(false, false,
getRetryHandler(), requestConfig) {
/**
* @param e 抛出的异常
* @param sameServer 是否同节点服务的重试
* @return
*/
@Override
public boolean isRetriableException(Throwable e, boolean sameServer) {
if (e instanceof ClientException) {
// 连接异常重试
if (((ClientException) e).getErrorType() == ClientException.ErrorType.CONNECT_EXCEPTION) {
return true;
}
// 连接超时重试
if (((ClientException) e).getErrorType() == ClientException.ErrorType.SOCKET_TIMEOUT_EXCEPTION) {
return true;
}
// 读超时重试,读超时重试只允许不同服务节点的重试
// 所以同节点的重试不支持,读超时了就不要重新请求同一个节点了。
if (((ClientException) e).getErrorType() == ClientException.ErrorType.READ_TIMEOUT_EXCEPTION) {
return !sameServer;
}
// 服务端异常
// 服务端异常切换新节点重试
if (((ClientException) e).getErrorType() == ClientException.ErrorType.SERVER_THROTTLED) {
return !sameServer;
}
}
// 连接异常时重试
return isConnectionException(e);
}
};
}
}
由于FeignLoadBalancer
是在OpenFeign
的LoadBalancerFeignClient
中调用一个CachingSpringLoadBalancerFactory
创建的,所以我们还需要替换OpenFeign
的FeignRibbonClientAutoConfiguration
配置类注册的CachingSpringLoadBalancerFactory
,就是自己创建CachingSpringLoadBalancerFactory并注册到Spring
容器。然后重写CachingSpringLoadBalancerFactory
的create
方法,代码如下。
@Configuration
public class RibbonConfiguration {
/**
* 使用自定义FeignLoadBalancer缓存工厂
*
* @return
*/
@Bean
public CachingSpringLoadBalancerFactory cachingSpringLoadBalancerFactory() {
return new CachingSpringLoadBalancerFactory(springClientFactory) {
private volatile Map cache = new ConcurrentReferenceHashMap<>();
@Override
public FeignLoadBalancer create(String clientName) {
FeignLoadBalancer client = this.cache.get(clientName);
if (client != null) {
return client;
}
IClientConfig config = this.factory.getClientConfig(clientName);
ILoadBalancer lb = this.factory.getLoadBalancer(clientName);
ServerIntrospector serverIntrospector = this.factory.getInstance(clientName,
ServerIntrospector.class);
// 使用自定义的FeignLoadBalancer
client = new MyFeignLoadBalancer(lb, config, serverIntrospector);
this.cache.put(clientName, client);
return client;
}
};
}
}
实测是生效的。这里有个坑,就是不能通过在CachingSpringLoadBalancerFactory
的create
方法中创建匿名内部类FeignLoadBalancer。
▼
往期原创精选
▼
OpenFeign与Ribbon源码分析总结(面试题)
Spring Cloud Ribbon源码分析(Spring Cloud Kubernetes)
Spring Cloud OpenFeign源码分析
Spring Cloud kubernetes入门项目sck-demo
为什么要选择Spring Cloud Kubernetes?
公众号:Java艺术
扫码关注最新动态