如题,本文基于Spring Cloud Finchley.SR2
对比Daltson和Finchley的基本组件,发现Ribbon还有Hystrix的重试逻辑基本没变,feign编程openfeign之后,增加了个重试逻辑,我们用下面这个图来展示其中的逻辑:
首先搞清楚调用链:
可以总结如下:
@LoadBalanced
注解修饰的RestTemplate是有作用的。RetryableFeignLoadBalancer
使用RetryTemplate实现了自己的重试逻辑,其中的RetryPolicy还是RibbonLoadBalancedRetryPolicy
,读取的配置还是ribbon.MaxAutoRetries
和ribbon.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.SynchronousMethodHandler
和feign.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
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之后这样阶梯式重试。