如題,本文基於Spring Cloud Finchley.SR2
OpenFeign的重試
OpenFeign配置重試後,邏輯分析
對比Daltson和Finchley的基本組件,發現Ribbon還有Hystrix的重試邏輯基本沒變,feign編程openfeign之後,增加了個重試邏輯,我們用下面這個圖來展示其中的邏輯:
首先搞清楚調用鏈:
可以總結如下:
- OpenFeign有自己的重試機制,重試的是整個後面的調用棧(也就是說,ribbon的重試又被整個重新重試了一遍)
- Ribbon通過增加Spring-retry還有相關配置開啓了重試,這個重試機制對於OpenFeign是不起作用的,但是對於
@LoadBalanced
註解修飾的RestTemplate是有作用的。 RetryableFeignLoadBalancer
使用RetryTemplate實現了自己的重試邏輯,其中的RetryPolicy還是RibbonLoadBalancedRetryPolicy
,讀取的配置還是ribbon.MaxAutoRetries
和ribbon.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.SynchronousMethodHandler
和feign.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
可能存在的缺陷:
- 對於默認的負載均衡規則基於RoundRobin,目前的代碼,如果
ribbon.MaxAutoRetries=0
,ribbon.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. 調用另一個重量級操作
2. 讀取數據庫
3. 返回拼裝結果
所以業務上最好設計時,先讀取公共模塊,之後再做重量級操作。技術上避免,就是通過Openfeign的Retryer的重試實現,如果業務上發現數據庫超時或者數據庫連接不上的異常,就返回503並且填充Retry-After
這個Header,讓Openfeign的Retryer過一會再重試
更好的方案是按照阿里重試方案,1s,2s,4s,8s之後這樣階梯式重試。