问题描述
我们在调试接口时,接口很容易超时,当然线上环境因为网络抖动、接口响应慢等,也造成接口超时,强大的feign
也提供了超时/失败重试功能,然而,请求重试必须建立在该接口具备幂等性
的前提下,一般情况下,Get
请求都是幂等的,但是如果是Post
请求呢,重试后会是什么结果?
如果接口没有保证幂等性
,那么重试Post
请求(假设重试2次),就相当于成功调用了2次接口,数据库也就多出了2条记录。这显然不是我们希望看到的。下面给出解决方案。
springcloud openfeign版本
spring-cloud-starter-openfeign:2.0.1.RELEASE
解决方案
1. 修改ribbon配置只针对Get请求重试
### 请求处理的超时时间
ribbon:
# 等待请求响应的超时时间. 单位:ms
ReadTimeout: 5000
# 连接超时时间. 单位:ms
ConnectTimeout: 1000
# 是否对所有请求进行失败重试, 设置为 false, 让feign只针对Get请求进行重试.
OkToRetryOnAllOperations: false
其实,Feign本身默认是没有开启重试的,具体可见下文的源码分析。
2. 修改ribbon的重试次数
ribbon:
# Max number of retries on the same server (excluding the first try)
MaxAutoRetries: 0
# Max number of next servers to retry (excluding the first server)
MaxAutoRetriesNextServer: 0
同一个服务实例的重试次数(MaxAutoRetries
)、不同服务实例(MaxAutoRetriesNextServer
)的重试次数都设置为0,即可达到不重试的目的。
3. 关闭重试机制
spring:
cloud:
loadbalancer:
retry:
enabled: false
终极大招,索性把重试直接关闭,不过这种是最不推荐的做法。
源码分析
Feign自带的重试机制
首先,在Spring Cloud OpenFeign中,入口为FeignClient
,源码如下:
所有属性性中,只有configuration
看着跟重试有点关系,再看到javadoc
提示的@see FeignClientsConfiguration for the defaults
,该配置为Feign
的默认配置,源码如下:
终于见到Retryer
本尊,可以看到,当容器中缺少实现Retryer
接口的Bean
时则自动生成实例并注入(@ConditionalOnMissingBean
的功劳),这里注入的是Retryer.NEVER_RETRY
,一个不进行重试的Retryer
实现类。实现为:
Retryer
有2个方法,重点放在continueOrPropagate(RetryableException e)
上,从方法签名上看,要么等待下一次重试,要么将异常Propagate(传播)出去,通俗点就是抛异常。
大家应该都知道,Feign
底层使用的是动态代理,才能实现如此简单易用的使用体验。这里主要讲一个类:SynchronousMethodHandler
,该类动态代理的主要类,所以它的属性跟Feign
的参数很相似,主要源码如下:
在invoke(Object[] argv)
方法中,当捕获RetryableException
异常时,会调用Retryer
的continueOrPropagate
方法,根据执行结果,该重试的重试,该抛异常的抛异常。
而根据NERVER_RETRY
的源码,就是直接抛异常,不进行重试。
那就奇怪了,既然重试策略为NERVER_RETRY
,为何接口调用超时还是会重试呢?
依赖Ribbon的重试机制
Spring Cloud OpenFeign
默认是使用Ribbon
实现负载均衡和重试机制的,虽然feign有自己的重试机制,但该功能在Spring Cloud OpenFeign
基本用不上,除非有特定的业务需求,则可以实现自己的Retryer
,然后在全局注入或者针对特定的客户端使用特定的Retryer
。
对于Spring Cloud OpenFeign
的重试机制,这里主要说明两个类:FeignLoadBalancer
、RequestSpecificRetryHandler
,关键代码如下:
首先分析一下接口RetryHandler
(属于ribbon
),关键方法isRetriableException(Throwable e, boolean sameServer)
,用于判断此次请求失败抛出的异常是否需要重试。其实现类RequestSpecificRetryHandler
有2个比较重要的参数:okToRetryOnAllErrors
、okToRetryOnConnectErrors
。
-
okToRetryOnAllErrors
:为true
时,无论是接口请求超时、服务端处理失败、建立连接失败等,统一返回true,即可以重试; -
okToRetryOnConnectErrors
:为true
时,只要是在跟服务端建立连接时出现错误,无论建立连接超时、建立连接失败等,统一返回true。
注:这里有2个超时概念,建立连接超时、接口请求超时。
- 建立连接超时: 是发生在与服务端建立连接时出现超时;对应
ribbon
配置:ribbon.ConnectTimeout- 接口请求超时是在连接建立成功的前提下,服务端处理超时、或接受响应超时等;对应
ribbon
配置:ribbon.ReadTimeout。
而
FeignLoadBalancer
在获取请求重试处理器时,根据不同情况实例化不同的
RequestSpecificRetryHandler
,首先
okToRetryOnConnectErrors
参数都为
true
;而
okToRetryOnAllErrors
参数,有2种情况:
第一种情况,当配置
ribbon.OkToRetryOnAllOperations
为
true
时,
okToRetryOnAllErrors
始终为
true
;
第二种情况:根据请求的
HTTP_METHOD
取不同的值,当为
Get
请求时为
true
,其他都为
false
,不难理解,
Get
请求一般都是幂等的,而其他请求则不一定。
因此,ribbon.OkToRetryOnAllOperations
这个参数,强烈建议设置为false
。
扩展
1. 对不同服务使用不同的重试机制
上面的配置ribbon.**
,即以ribbon
开头的配置,是针对全局的,如果需要对不同服务定制化配置呢?参考如下:
# 针对 app1
app1:
ribbon:
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 1
# 针对 app2
app2:
ribbon:
MaxAutoRetriesNextServer: 2
# 针对 app3
app3:
ribbon:
MaxAutoRetries: 1
- feign使用自定义配置(包含Retryer)
feign:
client:
config:
feignName:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
errorDecoder: com.example.SimpleErrorDecoder
retryer: com.example.SimpleRetryer
requestInterceptors:
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
decode404: false
encoder: com.example.SimpleEncoder
decoder: com.example.SimpleDecoder
contract: com.example.SimpleContract
上面的feignName,一般为远端服务的服务名。因为Spring Cloud 有自己的服务发现,通过服务名就能定位到该服务的可用实例列表,再通过负载均衡策略选取其中一个实例,最后向该服务实例发起请求。
上面的配置是针对某个服务的,而其他服务的配置可能基本都一样,这时,只需要将feignName
替换成default
即可。这样即可全局自定义配置。
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic
参考
spring-cloud-feign-overriding-defaults
https://github.com/Netflix/ribbon/wiki/Getting-Started
SpringCloud Feign重试详解
feign 的重试机制
推荐阅读
Spring Cloud 进阶玩法