十万个why:Feign 超时时间明明设了 3 秒,为什么实际等了 10 秒才返回超时?
大家好,我是小富~
《十万个why》系列持续更新中
这个问题我相信不少人遇到过。配置文件里写得清清楚楚:
feign:
client:
config:
default:
connectTimeout: 3000
readTimeout: 3000
下游服务一旦变慢或者挂掉,按预期最多 3 秒就应该超时返回。但实际一看监控,超时时间稳定在 10 秒左右。3 秒变 10 秒,多出来的 7 秒是哪来的?
先搞清楚一件事:你配的 Feign 超时,可能根本没生效!!!
很多人不知道,在 Spring Cloud 2020.0 之前的版本(对应 Spring Boot 2.4 之前),Feign 底层的 HTTP 调用不是自己发出去的,而是委托给 Ribbon 的 LoadBalancerFeignClient 来执行的。
你可以在项目里查一下:
mvn dependency:tree | grep ribbon
如果看到spring-cloud-starter-netflix-ribbon,那就对了,Ribbon 是实际干活的那个。
而 Ribbon 有自己的超时配置,默认值写在 DefaultClientConfigImpl 里:
public static final int DEFAULT_CONNECT_TIMEOUT = 2000;
public static final int DEFAULT_READ_TIMEOUT = 5000;
ReadTimeout 默认 5000 毫秒。你在 Feign 层配的 3000ms,到 Ribbon 这层,被它自己的默认值 5000ms 盖掉了。
但 5 秒也不是 10 秒,还差一半,问题出在哪?另一半时间:Ribbon 的默认重试
Ribbon 有两个和重试相关的参数:
// 同一台实例重试次数(不含首次)
ribbon.MaxAutoRetries = 0
// 切换下一台实例的重试次数
ribbon.MaxAutoRetriesNextServer = 1
MaxAutoRetriesNextServer = 1 意味着:第一台实例超时之后,Ribbon 会自动切换到另一台实例再试一次。
实际最大等待时间的计算公式:总耗时 = ReadTimeout × (MaxAutoRetries + 1) × (MaxAutoRetriesNextServer + 1)
代入默认值:5000 × (0 + 1) × (1 + 1) = 5000 × 1 × 2 = 10000ms
10 秒,对上了。
第一台实例等 5 秒超时,切到第二台再等 5 秒超时,一共 10 秒。整个过程中你配的那个 3 秒没有参与任何环节。
为什么 Ribbon 能覆盖 Feign 的配置?
不是猜的,看源码。
FeignLoadBalancer 在执行请求时构造超时参数的逻辑:
@Override
public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
throws IOException {
Request.Options options;
if (configOverride != null) {
RibbonProperties override = RibbonProperties.from(configOverride);
options = new Request.Options(
override.connectTimeout(this.connectTimeout),
override.readTimeout(this.readTimeout)
);
} else {
options = new Request.Options(this.connectTimeout, this.readTimeout);
}
// ...
}
this.connectTimeout 和 this.readTimeout 来自 Ribbon 的 IClientConfig,不是来自 Feign 的配置。Feign 层面设的超时值在这里被直接无视了。
配置优先级实际上是这样的:Ribbon 服务级别配置 > Ribbon 全局配置 > Ribbon 默认值 >> Feign 配置(不生效)
怎么改
知道原因了,改法很直接。
第一种:配 Ribbon 的参数
既然实际生效的是 Ribbon,就直接配 Ribbon:
ribbon:
ConnectTimeout: 3000
ReadTimeout: 3000
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 0
MaxAutoRetriesNextServer 建议改成 0。默认的重试行为对 GET 请求问题不大,但如果是 POST 请求,没有幂等保证的情况下重试一次,可能直接造成业务重复——比如多扣一笔款、多发一条短信。
需要对特定服务单独设置的话:
order-service:
ribbon:
ReadTimeout: 5000
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 0
第二种:升级 Spring Cloud,去掉 Ribbon
Spring Cloud 2020.0 开始,Ribbon 被移除了,替代方案是 Spring Cloud LoadBalancer。升级之后 Feign 的超时配置才是真正生效的那一份:
feign:
client:
config:
default:
connectTimeout: 3000
readTimeout: 3000
如果项目短期内升不上去,就老实配 Ribbon,不要在 Feign 层面配了之后以为万事大吉。
第三种:在上层用熔断器兜底
如果你对底层超时配置的优先级已经搞不清了,可以在 Feign 外面再套一层 Sentinel 或者 Resilience4j 做超时控制。
不管底层实际等多久,上层到了时间直接走 fallback。多一层封装,但确定性强。
怎么验证配置到底生效没有
写一个测试接口,故意 sleep 超过超时时间:
@GetMapping("/slow")
public String slow() throws InterruptedException {
Thread.sleep(15000);
return "done";
}
用 Feign 调这个接口,看实际多久返回异常:
约 3 秒返回,你的超时配置生效了
约 5 秒返回,Ribbon 默认超时在生效,重试关了
约 10 秒返回,Ribbon 默认超时 + 默认重试,你配的 Feign 超时完全没用
不用猜,跑一次就知道了。
说在最后
这个问题的根源就一句话:在有 Ribbon 的 Spring Cloud 项目里,Feign 的超时配置是不生效的,实际控制超时的是 Ribbon。
配超时的时候,先搞清楚最终是谁在发 HTTP 请求,然后去配那一层的参数。中间封装了几层,就有几层你以为生效了其实没生效的可能。
