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

关注“Java艺术”一起来充电吧!

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

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

  • Ribbon的重试策略配置

  • 如何替换RetryHandler

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

Feign会为每个服务提供者创建一个Client发起请求,Client即服务消费端。我们把每个服务分为服务提供端Server和服务消费端Client

  • 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的配置与源码分析_第1张图片

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

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

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

@Configuration
public class RibbonAutoConfiguration{
    // 创建RibbonLoadBalancerClient
    @Bean
	@ConditionalOnMissingBean(LoadBalancerClient.class)
	public LoadBalancerClient loadBalancerClient() {
		return new RibbonLoadBalancerClient(springClientFactory());
	}
}

SpringClientFactoryRibbon使用的ApplicationContextRibbon会为每个Client都创建一个AnnotationConfigApplicationContext,用作环境隔离。SpringClientFactory与每个ClientAnnotationConfigApplicationContext的关系如下图所示。

Ribbon重试策略RetryHandler的配置与源码分析_第2张图片

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

public class SpringClientFactory extends NamedContextFactory{

	public SpringClientFactory() {
		super(RibbonClientConfiguration.class, NAMESPACE, "ribbon.client.name");
	}
}

RibbonClientConfiguration配置类在每个Client对应的AnnotationConfigApplicationContext初始化时生效,每个ClientAnnotationConfigApplicationContext都是在第一次调用服务的接口时才被创建。创建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时,真正使用的RetryHandlerRequestSpecificRetryHandler。前面我们分析OpenFeign整合Ribbon源码时提到一个启到桥接作用的类:FeignLoadBalancer

Ribbon重试策略RetryHandler的配置与源码分析_第3张图片

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 command = buildLoadBalancerCommand(request, requestConfig);
        try {
            return command.submit({....})
                .toBlocking()
                .single();
        } catch (Exception e) {
        }
    }
}

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

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

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

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是怎么来的呢?

FeignLoadBalancerexecuteWithLoadBalancer方法中调用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使用的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接口的定义如下图所示。

Ribbon重试策略RetryHandler的配置与源码分析_第4张图片

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方法下断点调试,如下图所示。

Ribbon重试策略RetryHandler的配置与源码分析_第5张图片

从图中可以看出,配置参数key的格式为:

<服务提供者的名称(serverId)>::<参数名>=

假设我们配置调用服务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

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

既然使用的是FeignLoadBalancergetRequestSpecificRetryHandler方法返回的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是在OpenFeignLoadBalancerFeignClient中调用一个CachingSpringLoadBalancerFactory创建的,所以我们还需要替换OpenFeignFeignRibbonClientAutoConfiguration配置类注册的CachingSpringLoadBalancerFactory,就是自己创建CachingSpringLoadBalancerFactory并注册到Spring容器。然后CachingSpringLoadBalancerFactorycreate方法,代码如下。

@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;
            }
        };
    }
}

实测是生效的。这里有个坑,就是不能通过在CachingSpringLoadBalancerFactorycreate方法中创建匿名内部类FeignLoadBalancer

往期原创精选

OpenFeign与Ribbon源码分析总结(面试题)

Spring Cloud Ribbon源码分析(Spring Cloud Kubernetes)

Spring Cloud OpenFeign源码分析

Spring Cloud kubernetes入门项目sck-demo

为什么要选择Spring Cloud Kubernetes?

公众号:Java艺术

扫码关注最新动态

你可能感兴趣的:(Ribbon重试策略RetryHandler的配置与源码分析)