Spring Cloud Finchley OpenFeign的重试配置相关的坑

如题,本文基于Spring Cloud Finchley.SR2

OpenFeign的重试

OpenFeign配置重试后,逻辑分析

对比Daltson和Finchley的基本组件,发现Ribbon还有Hystrix的重试逻辑基本没变,feign编程openfeign之后,增加了个重试逻辑,我们用下面这个图来展示其中的逻辑:

首先搞清楚调用链:
Spring Cloud Finchley OpenFeign的重试配置相关的坑_第1张图片

可以总结如下:

  1. OpenFeign有自己的重试机制,重试的是整个后面的调用栈(也就是说,ribbon的重试又被整个重新重试了一遍)
  2. Ribbon通过增加Spring-retry还有相关配置开启了重试,这个重试机制对于OpenFeign是不起作用的,但是对于@LoadBalanced注解修饰的RestTemplate是有作用的。
  3. RetryableFeignLoadBalancer使用RetryTemplate实现了自己的重试逻辑,其中的RetryPolicy还是RibbonLoadBalancedRetryPolicy,读取的配置还是ribbon.MaxAutoRetriesribbon.MaxAutoRetriesNextServer,所以其实这两个配置是在这里起作用了。

我们来看下代码实现:
首先是Ribbon的重试(LoadBalancerCommand)

public Observable submit(final ServerOperation operation) {
    //省略无关紧要的代码
    //获取配置:ribbon.MaxAutoRetries和ribbon.MaxAutoRetriesNextServer
    //每台服务器最多重试次数,但是首次调用不包括在内
    final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer();
    //最多重试多少台服务器,但是首次调用不包括在内
    final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer();

    // Use the load balancer
    Observable o = 
            //通过负载均衡器获取一个Server执行请求
            (server == null ? selectServer() : Observable.just(server))
            .concatMap(new Func1>() {
                @Override
                // Called for each server being selected
                public Observable call(Server server) {
                    context.setServer(server);
                    final ServerStats stats = loadBalancerContext.getServerStats(server);
                    
                    //对于每次重试,都要走的逻辑
                    Observable o = Observable
                            .just(server)
                            .concatMap(new Func1>() {
                                @Override
                                public Observable call(final Server server) {
                                    
                                    context.incAttemptCount();
                                    //省略无关代码
                                    //operation.call(server)就是调用RetryableFeignLoadBalancer的execute方法
                                    //但外层有封装方法把它返回的结果封装成了rxjava的Observable
                                    //这里针对这个Observable增加回调
                                    //这些回调其实就是记录一些调用数据,用于负载均衡规则选择server
                                    return operation.call(server).doOnEach(new Observer() {
                                        //省略实现
                                    });
                                }
                            });
                    
                    if (maxRetrysSame > 0) 
                        o = o.retry(retryPolicy(maxRetrysSame, true));
                    return o;
                }
            });
    
    //补充对于尝试下一个server的逻辑    
    if (maxRetrysNext > 0 && server == null) 
        o = o.retry(retryPolicy(maxRetrysNext, false));
    
    //在有异常的时候,判断是否超过重试次数
    return o.onErrorResumeNext(new Func1>() {
        @Override
        public Observable call(Throwable e) {
            if (context.getAttemptCount() > 0) {
                if (maxRetrysNext > 0 && context.getServerAttemptCount() == (maxRetrysNext + 1)) {
                    e = new ClientException(ClientException.ErrorType.NUMBEROF_RETRIES_NEXTSERVER_EXCEEDED,
                            "Number of retries on next server exceeded max " + maxRetrysNext
                            + " retries, while making a call for: " + context.getServer(), e);
                }
                else if (maxRetrysSame > 0 && context.getAttemptCount() == (maxRetrysSame + 1)) {
                    e = new ClientException(ClientException.ErrorType.NUMBEROF_RETRIES_EXEEDED,
                            "Number of retries exceeded max " + maxRetrysSame
                            + " retries, while making a call for: " + context.getServer(), e);
                }
            }
            if (listenerInvoker != null) {
                listenerInvoker.onExecutionFailed(e, context.toFinalExecutionInfo());
            }
            return Observable.error(e);
        }
    });
}

可以看出,必须调用的operation.call(server)有异常走到ERROR处理逻辑才会走这里的重试。但是我们看RetryableFeignLoadBalancer的源代码可以发现,RetryableFeignLoadBalancer用的RetryTemplate实现了自己的重试,根本不会将异常抛出来到外层。

然后是OpenFeign的执行(RetryableFeignLoadBalancer):

public RibbonResponse execute(final RibbonRequest request, IClientConfig configOverride) throws IOException {
    //省略无关代码
    //读取ribbon.MaxAutoRetries和ribbon.MaxAutoRetriesNextServer生成RetryPolicy用于之后的RetryTemplate重试
    final LoadBalancedRetryPolicy retryPolicy = this.loadBalancedRetryFactory.createRetryPolicy(this.getClientName(), this);
    RetryTemplate retryTemplate = new RetryTemplate();
    BackOffPolicy backOffPolicy = this.loadBalancedRetryFactory.createBackOffPolicy(this.getClientName());
    retryTemplate.setBackOffPolicy((BackOffPolicy)(backOffPolicy == null ? new NoBackOffPolicy() : backOffPolicy));
    RetryListener[] retryListeners = this.loadBalancedRetryFactory.createRetryListeners(this.getClientName());
    if (retryListeners != null && retryListeners.length != 0) {
        retryTemplate.setListeners(retryListeners);
    }

    retryTemplate.setRetryPolicy((RetryPolicy)(retryPolicy == null ? new NeverRetryPolicy() : new FeignRetryPolicy(request.toHttpRequest(), retryPolicy, this, this.getClientName())));
    return (RibbonResponse)retryTemplate.execute(new RetryCallback() {
        public RibbonResponse doWithRetry(RetryContext retryContext) throws IOException {
            Request feignRequest = null;
            if (retryContext instanceof LoadBalancedRetryContext) {
                ServiceInstance service = ((LoadBalancedRetryContext)retryContext).getServiceInstance();
                if (service != null) {
                    feignRequest = ((RibbonRequest)request.replaceUri(RetryableFeignLoadBalancer.this.reconstructURIWithServer(new Server(service.getHost(), service.getPort()), request.getUri()))).toRequest();
                }
            }

            if (feignRequest == null) {
                feignRequest = request.toRequest();
            }

            Response response = request.client().execute(feignRequest, options);
            //判断ribbon.retryableStatusCodes的状态码是否包含返回码,如果包含则抛出异常
            //不包含就返回封装的response,抛出异常会直接根据RetryPolicy进行重试
            //这里的RetryPolicy就是之前说的RibbonLoadBalancedRetryPolicy
            if (retryPolicy.retryableStatusCode(response.status())) {
                byte[] byteArray = response.body() == null ? new byte[0] : StreamUtils.copyToByteArray(response.body().asInputStream());
                response.close();
                throw new RibbonResponseStatusCodeException(RetryableFeignLoadBalancer.this.clientName, response, byteArray, request.getUri());
            } else {
                return new RibbonResponse(request.getUri(), response);
            }
        }
    }, new LoadBalancedRecoveryCallback() {
        protected RibbonResponse createResponse(Response response, URI uri) {
            return new RibbonResponse(uri, response);
        }
    });
}

最后OpenFeign的Retryer重试在哪里执行呢?就是在拿到Response之后,判断Response的header里面是否有Retry-After这个Header,如果有,就按照Retryer的配置进行重试,这个重试会重新调用整个调用栈进行重试(源代码略,参考feign.SynchronousMethodHandlerfeign.codec.ErrorDecoder

配置总结与目前的缺陷

目前实现的配置是,本机不重试,最多重试另一台机器,只对GET请求的返回码为500的请求重试,不考虑Retry-After这个Header

maven依赖(除了SpringCloud基本依赖):


    org.springframework.retry
    spring-retry
    1.2.4.RELEASE

application.properties配置:

#开启hystrix
feign.hystrix.enabled=true
#关闭断路器
hystrix.command.default.circuitBreaker.enabled=false
#禁用hystrix远程调用超时时间
hystrix.command.default.execution.timeout.enabled=false
hystrix.threadpool.default.coreSize=50
#ribbon连接超时
ribbon.ConnectTimeout=500
#ribbon读超时
ribbon.ReadTimeout=8000
#最多重试多少台服务器,但是首次调用不包括在内
ribbon.MaxAutoRetriesNextServer=1
#每台服务器最多重试次数,但是首次调用不包括在内
ribbon.MaxAutoRetries=0
#需要重试的状态码
ribbon.retryableStatusCodes=500

可能存在的缺陷:

  1. 对于默认的负载均衡规则基于RoundRobin,目前的代码,如果ribbon.MaxAutoRetries=0ribbon.MaxAutoRetriesNextServer=1,也会调用两次获取Server,这样如果集群正好只有两个,客户端只有一个,可能会出现一直重试调用同一台机器的情况,所以,负载均衡规则最好选择com.netflix.loadbalancer.AvailabilityFilteringRule,对于这个Rule,我另一篇文章做了分析,加上配置:
你的微服务名.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.AvailabilityFilteringRule

# 单实例最大活跃链接个数
niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit=50


#eureka客户端ribbon刷新时间
#默认30s
ribbon.ServerListRefreshInterval=1000

# ribbon.ServerListRefreshInterval时间内有多少断路次数就触发断路机制(以下配置都是默认值,可以不配置,这里只是为了说明)
niws.loadbalancer.你的微服务名.connectionFailureCountThreshold=3
niws.loadbalancer.你的微服务名.circuitTripTimeoutFactorSeconds=10
niws.loadbalancer.你的微服务名.circuitTripMaxTimeoutSeconds=30

这样就算一直重试同一台,也会让这台机器快速断路(一种是连接不上抛出SocketException或者是调用超时SocketTimeoutException,还有就是活跃请求过多)。

  1. 对于非微服务不可用,而是共用的某个模块不可用,例如数据库,这么做重试可能会导致雪崩现象的出现,例如某个接口逻辑是:
1. 调用另一个重量级操作
2. 读取数据库
3. 返回拼装结果

所以业务上最好设计时,先读取公共模块,之后再做重量级操作。技术上避免,就是通过Openfeign的Retryer的重试实现,如果业务上发现数据库超时或者数据库连接不上的异常,就返回503并且填充Retry-After这个Header,让Openfeign的Retryer过一会再重试
更好的方案是按照阿里重试方案,1s,2s,4s,8s之后这样阶梯式重试

你可能感兴趣的:(Spring,Cloud,Spring,Cloud相关)