Spring Cloud Finchley OpenFeign的重試配置相關的坑

如題,本文基於Spring Cloud Finchley.SR2

OpenFeign的重試

OpenFeign配置重試後,邏輯分析

對比Daltson和Finchley的基本組件,發現Ribbon還有Hystrix的重試邏輯基本沒變,feign編程openfeign之後,增加了個重試邏輯,我們用下面這個圖來展示其中的邏輯:

首先搞清楚調用鏈:
image

可以總結如下:

  1. OpenFeign有自己的重試機制,重試的是整個後面的調用棧(也就是說,ribbon的重試又被整個重新重試了一遍)
  2. Ribbon通過增加Spring-retry還有相關配置開啓了重試,這個重試機制對於OpenFeign是不起作用的,但是對於@LoadBalanced註解修飾的RestTemplate是有作用的。
  3. RetryableFeignLoadBalancer使用RetryTemplate實現了自己的重試邏輯,其中的RetryPolicy還是RibbonLoadBalancedRetryPolicy,讀取的配置還是ribbon.MaxAutoRetriesribbon.MaxAutoRetriesNextServer,所以其實這兩個配置是在這裏起作用了。

我們來看下代碼實現:
首先是Ribbon的重試(LoadBalancerCommand)

public Observable<T> submit(final ServerOperation<T> operation) {
    //省略無關緊要的代碼
    //獲取配置:ribbon.MaxAutoRetries和ribbon.MaxAutoRetriesNextServer
    //每臺服務器最多重試次數,但是首次調用不包括在內
    final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer();
    //最多重試多少臺服務器,但是首次調用不包括在內
    final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer();

    // Use the load balancer
    Observable<T> o = 
            //通過負載均衡器獲取一個Server執行請求
            (server == null ? selectServer() : Observable.just(server))
            .concatMap(new Func1<Server, Observable<T>>() {
                @Override
                // Called for each server being selected
                public Observable<T> call(Server server) {
                    context.setServer(server);
                    final ServerStats stats = loadBalancerContext.getServerStats(server);
                    
                    //對於每次重試,都要走的邏輯
                    Observable<T> o = Observable
                            .just(server)
                            .concatMap(new Func1<Server, Observable<T>>() {
                                @Override
                                public Observable<T> call(final Server server) {
                                    
                                    context.incAttemptCount();
                                    //省略無關代碼
                                    //operation.call(server)就是調用RetryableFeignLoadBalancer的execute方法
                                    //但外層有封裝方法把它返回的結果封裝成了rxjava的Observable
                                    //這裏針對這個Observable增加回調
                                    //這些回調其實就是記錄一些調用數據,用於負載均衡規則選擇server
                                    return operation.call(server).doOnEach(new Observer<T>() {
                                        //省略實現
                                    });
                                }
                            });
                    
                    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<Throwable, Observable<T>>() {
        @Override
        public Observable<T> 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<RibbonResponse, IOException>() {
        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<RibbonResponse, Response>() {
        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基本依賴):

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.2.4.RELEASE</version>
</dependency>

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之後這樣階梯式重試

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章