Ribbon重试策略RetryHandler的配置与源码分析

在《OpenFeignRibbon源码分析总结》这篇文章中,我们只是简单地了解Ribbon的重试机制的实现原理,本篇我们再对Ribbon的重试机制地实现做详细分析,从源码分析找出我们想要地答案,即如何配置Ribbon实现调用每个服务的接口使用不一样的重试策略,如配置失败重试多少次,以及自定义重试策略RetryHandler

  • Ribbon重试机制地实现源码分析

  • Ribbon的重试策略配置

  • 如何替换RetryHandler

本篇源码分析部分涉及到的关键类说明

  • LoadBalancerFeignClientOpenFeign整合Ribbon时使用的ClientOpenFeign使用Client发送请求);

  • FeignLoadBalancerOpenFeign整合Ribbon的桥接器,由LoadBalancerFeignClient创建;

  • LoadBalancerCommandRibbon将请求转为RxJava API调用的实现,由FeignLoadBalancer调用;

  • CachingSpringLoadBalancerFactoryOpenFeign整合Ribbon用于创建FeignLoadBalancer桥接器的带缓存功能的FeignLoadBalancer工厂。

  • RibbonLoadBalancerClientRibbon提供的实现Spring Cloud负载均衡接口(LoadBalancerClient)的类;

  • RibbonAutoConfigurationRibbon的自动配置类,注册RibbonLoadBalancerClientSpring容器。

  • SpringClientFactoryRibbon自己管理一群ApplicationContextRibbon会为每个Client创建一个ApplicationContext

  • RibbonClientConfigurationRibbon为每个Client提供ApplicationContext实现环境隔离,这是Ribbon为每个Client创建ApplicationContext时都使用的配置类,用于注册Ribbon的各种功能组件,如负载均衡器ILoadBalancer

  • RequestSpecificRetryHandlerRetryHandler接口的实现类,OpenFeign整合Ribbon使用的默认失败重试策略处理器;

Ribbon重试机制地实现源码分析

Ribbon的重试机制使用了RxJavaAPI,而重试次数以及是否重试的决策由RetryHandler实现。Ribbon提供两个RetryHandler的实现类,如下图所示。

现在我们要找出Ribbon使用的是哪个RetryHandler,我们只分析OpenFeignRibbon整合的使用,Spring Cloud@LoadBalanced注解方式使用我们不做分析。

spring-cloud-netflix-ribbonspring.factories文件导入的自动配置类是RibbonAutoConfiguration,该配置类向Spring容器注入了一个RibbonLoadBalancerClientRibbonLoadBalancerClient正是RibbonSpring Cloud的负载均衡接口提供的实现类。

在创建RibbonLoadBalancerClient时给构造方法传入了一个SpringClientFactory,源码如下。

@Configuration

publicclassRibbonAutoConfiguration{

// 创建RibbonLoadBalancerClient

@Bean

@ConditionalOnMissingBean(LoadBalancerClient.class)

public LoadBalancerClient loadBalancerClient(){

returnnew RibbonLoadBalancerClient(springClientFactory());

}

}

SpringClientFactoryRibbon使用的ApplicationContextRibbon会为每个Client都创建一个AnnotationConfigApplicationContext,用作环境隔离。

SpringClientFactory在调用父类构造方法时传入了一个配置类:RibbonClientConfiguration,源码如下。

publicclassSpringClientFactoryextendsNamedContextFactory<RibbonClientSpecification>{

publicSpringClientFactory(){

super(RibbonClientConfiguration.class, NAMESPACE, "ribbon.client.name");

}

}

RibbonClientConfiguration配置类在每个Client对应的AnnotationConfigApplicationContext初始化时生效,在第一次调用服务的接口时AnnotationConfigApplicationContext才被创建。创建ApplicationContext并且调用register方法注册RibbonClientConfiguration配置类以及其它一些配置类,最后调用其refresh方法初始化该ApplicationContext

RibbonClientConfiguration负责为每个Client对应的ApplicationContext注入服务列表ServerList<Server>、服务列表更新器ServerListUpdater、负载均衡器ILoadBalancer、负载均衡算法IRule、客户端配置IClientConfig、重试决策处理器RetryHandler等。

  • 服务列表ServerList<Server>:从注册中心获取可用服务提供者节点;
  • 服务列表更新器ServerListUpdater:定时更新本地缓存的服务列表,调用ServerList从注册中心获取;
  • 负载均衡算法IRule:实现各种负载均衡算法,如随机、轮询等;
  • 负载均衡器ILoadBalancer:调用负载均衡算法IRule选择一个服务提供者节点调用;
  • 重试决策处理器RetryHandler:决定本次失败是否重试;

由于RibbonClientConfiguration注册的Bean是注册在Client隔离的ApplicationContext中的, 所以调用每个服务提供者的接口将可以使用不同的客户端配置(IClientConfig)、重试决策处理器(RetryHandler)等。这也是我们能够为Ribbon配置调用每个服务的接口使用不一样的重试策略的前提条件,不过这也不是充分必要条件。

RibbonClientConfiguration配置类会注册一个重试决策处理器RetryHandler,但这个RetryHandler并未被使用,也可能是别的地方使用。

@Configuration

publicclassRibbonClientConfiguration{

// 未使用

@Bean

@ConditionalOnMissingBean

public RetryHandler retryHandler(IClientConfig config){

returnnew DefaultLoadBalancerRetryHandler(config);

}

}

OpenFeign整合Ribbon时,真正使用的RetryHandlerRequestSpecificRetryHandler。前面我们分析OpenFeign整合Ribbon源码时提到一个启到桥接作用的类:FeignLoadBalancer

OpenFeign整合Ribbon使用时,OpenFeigin使用的ClientLoadBalancerFeignClient,由LoadBalancerFeignClient创建FeignLoadBalancer,并调用FeignLoadBalancerexecuteWithLoadBalancer方法实现负载均衡调用。

executeWithLoadBalancer方法实际是FeignLoadBalancer的父类AbstractLoadBalancerAwareClient提供的方法,其源码如下(有删减)。

public abstract class AbstractLoadBalancerAwareClient{

public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {

LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);

try {

return command.submit({....})

.toBlocking()

.single();

} catch (Exception e) {

}

}

}

executeWithLoadBalancer方法中会创建一个LoadBalancerCommand,然后调用LoadBalancerCommandsubmit方法提交请求,submit方法源码如下(有删减):

   public Observable<T> submit(final ServerOperation<T> operation) {

// .......

// &emsp;获取重试次数

final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer();

final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer();

// Use the load balancer

Observable<T> o = (server == null ? selectServer() : Observable.just(server))

.concatMap(new Func1<Server, Observable<T>>() {

@Override

public Observable<T> 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方法中调用retryHandlergetMaxRetriesOnSameServer方法和getMaxRetriesOnNextServer方法分别获取配置maxRetrysSamemaxRetrysNextmaxRetrysSame表示调用相同节点的重试次数,默认为0maxRetrysNext表示调用不同节点的重试次数,默认为1

retryPolicy方法返回的是一个包装RetryHandler重试决策者的RxJava API的对象,最终由该RetryHandler决定是否需要重试,如抛出的异常是否允许重试。而是否达到最大重试次数则是在retryPolicy返回的Func2中完成,这是RxJavaAPIretryPolicy方法的源码如下。

private Func2<Integer, Throwable, Boolean> retryPolicy(final int maxRetrys, final boolean same) {

return new Func2<Integer, Throwable, Boolean>() {

@Override

public Boolean call(Integer tryCount, Throwable e) {

if (e instanceof AbortExecutionException) {

returnfalse;

}

// 大于最大重试次数

if (tryCount > maxRetrys) {

returnfalse;

}

if (e.getCause() != null && e instanceof RuntimeException) {

e = e.getCause();

}

// 调用RetryHandler判断是否重试

return retryHandler.isRetriableException(e, same);

}

};

}

那么这个retryHandler是怎么来的呢?

FeignLoadBalancerexecuteWithLoadBalancer方法中调用buildLoadBalancerCommand方法构造LoadBalancerCommand对象时创建的,buildLoadBalancerCommand方法源码如下。

    protected LoadBalancerCommand<T> buildLoadBalancerCommand(final S request, final IClientConfig config) {

// 获取RetryHandler

RequestSpecificRetryHandler handler = getRequestSpecificRetryHandler(request, config);

// 使用Builder构造者模式构造LoadBalancerCommand

LoadBalancerCommand.Builder<T> builder = LoadBalancerCommand.<T>builder()

.withLoadBalancerContext(this)

// 传入RetryHandler

.withRetryHandler(handler)

.withLoadBalancerURI(request.getUri());

return builder.build();

}

从源码中可以看出,Ribbon使用的RetryHandlerRequestSpecificRetryHandler。这里还用到了Builder构造者模式。

FeignLoadBalancergetRequestSpecificRetryHandler方法源码如下:

@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的父RetryHandlerDefaultLoadBalancerRetryHandler

RetryHandler接口的定义如下图所示。

RetryHandler接口方法说明:

  • isRetriableException方法:该异常是否可重试;

  • isCircuitTrippingException方法:是否是Circuit熔断类型异常;

  • getMaxRetriesOnSameServer方法:调用同一节点的最大重试次数;

  • getMaxRetriesOnNextServer方法:调用不同节点的最大重试次数;

Ribbon的重试策略配置

最大重试次数、连接超时等参数的配置

FeignLoadBalancer在创建RequestSpecificRetryHandler时传入了IClientConfig,这个IClientConfig是从哪里创建的我们稍会再分析。RequestSpecificRetryHandler在构造方法中从这个IClientConfig中获取调用同服务节点的最大重试次数和调用不同服务节点的最大重试次数,源码如下。

publicclassRequestSpecificRetryHandlerimplementsRetryHandler{

publicRequestSpecificRetryHandler(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中配置的,其源码如下:

publicclassRibbonClientConfiguration{

// 默认连接超时

publicstaticfinalint DEFAULT_CONNECT_TIMEOUT = 1000;

// 默认读超时

publicstaticfinalint 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)>:<ribbon>:<参数名>=<value>

假设我们针对服务提供者sck-demo-provider配置最大同节点重试次数为10,配置最大不同节点重试次数为12,配置连接超时为15秒,那么我们需要在application-[环境].yaml配置文件中添加如下配置。

sck-demo-provider:

ribbon:

MaxAutoRetries: 10

MaxAutoRetriesNextServer: 12

ConnectTimeout: 15000

其中MaxAutoRetriesMaxAutoRetriesNextServer都能生效,但是ConnectTimeout配置是不生效的,原因是在RibbonClientConfiguration中创建DefaultClientConfigImpl时,先调用loadProperties方法(传入的name参数就是服务名称)从配置文件获取配置,再调用set方法覆盖了三个配置:连接超时配置、读超时配置、是否开启gzip压缩配置。所以这种方式配置连接超是不生效的。

第二种方法:代码配置

代码配置就是我们手动注册IClientConfig,而不使用RibbonClientConfiguration自动注册的。RibbonClientConfiguration自动注册IClientConfig的方法上添加了@ConditionalOnMissingBean条件注解,正因为如此,我们才可以自己注册IClientConfig

但要注意一点,RibbonClientConfiguration是在Ribbon为每个Client创建的ApplicationContext中生效的,所以我们需要创建一个配置类(Configuration),并将其注册到SpringClientFactory。这样,在SpringClientFactoryClient创建ApplicationContext时,就会将配置类注册到ApplicationContext,向SpringClientFactory注册的配置类也就成了创建的ApplicationContext的配置类。

@Configuration

publicclassRibbonConfigurationimplementsInitializingBean{

@Resource

private SpringClientFactory springClientFactory;

@Override

publicvoidafterPropertiesSet()throws Exception {

List<RibbonClientSpecification> 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)

publicstaticclassRibbonClientCfg{

@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了。

如何替换RetryHandler

OpenFeign整合Ribbon使用时,默认使用的是FeignLoadBalancergetRequestSpecificRetryHandler方法创建的RequestSpecificRetryHandler,笔者也看了一圈源码,实在找不到怎么替换RetryHandler,可能OpenFeign就是不想给我们替换吧。这种情况我们只能另寻辟径了。

既然使用的是FeignLoadBalancergetRequestSpecificRetryHandler方法返回的RetryHandler,那么我们是不是可以继承FeignLoadBalancer并重写getRequestSpecificRetryHandler方法来替换RetryHandler呢?答案是可以的。

自定义的FeignLoadBalancer代码如下:

/**

* 自定义FeignLoadBalancer,替换默认的RequestSpecificRetryHandler

*/

publicstaticclassMyFeignLoadBalancerextendsFeignLoadBalancer{

publicMyFeignLoadBalancer(ILoadBalancer lb, IClientConfig clientConfig, ServerIntrospector serverIntrospector){

super(lb, clientConfig, serverIntrospector);

}

@Override

public RequestSpecificRetryHandler getRequestSpecificRetryHandler(RibbonRequest request, IClientConfig requestConfig){

// 返回自定义的RequestSpecificRetryHandler

// 参数一:是否连接异常重试时重试

// 参数二:是否所有异常都重试

returnnew RequestSpecificRetryHandler(false, false,

getRetryHandler(), requestConfig) {

/**

* @param e 抛出的异常

* @param sameServer 是否同节点服务的重试

* @return

*/

@Override

publicbooleanisRetriableException(Throwable e, boolean sameServer){

if (e instanceof ClientException) {

// 连接异常重试

if (((ClientException) e).getErrorType() == ClientException.ErrorType.CONNECT_EXCEPTION) {

returntrue;

}

// 连接超时重试

if (((ClientException) e).getErrorType() == ClientException.ErrorType.SOCKET_TIMEOUT_EXCEPTION) {

returntrue;

}

// 读超时重试,读超时重试只允许不同服务节点的重试

// 所以同节点的重试不支持,读超时了就不要重新请求同一个节点了。

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是在OpenFeignLoadBalancerFeignClient中调用一个CachingSpringLoadBalancerFactory创建的,所以我们还需要替换OpenFeignFeignRibbonClientAutoConfiguration配置类注册的CachingSpringLoadBalancerFactory,并且重写CachingSpringLoadBalancerFactorycreate方法,代码如下。

@Configuration

public class RibbonConfiguration {

/**

* 使用自定义FeignLoadBalancer缓存工厂

*

* @return

*/

@Bean

public CachingSpringLoadBalancerFactory cachingSpringLoadBalancerFactory() {

return new CachingSpringLoadBalancerFactory(springClientFactory) {

private volatile Map<String, FeignLoadBalancer> 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;

}

};

}

}

以上是 Ribbon重试策略RetryHandler的配置与源码分析 的全部内容, 来源链接: utcz.com/a/28248.html

回到顶部