我为什么把“接口超时”从默认值改成了必填项
我为什么把“接口超时”从默认值改成了必填项
刚入行那阵,我调外部接口从来不配超时时间。不是不知道有这个参数,是觉得“默认就行”——HttpClient 默认没超时,我就一直等;Feign 默认 10 秒,那就等 10 秒呗。直到那次线上事故,让我把“超时配置”写进了团队开发规范的第二条(第一条是接口幂等)。
那是一个周五下午,我们的下单接口突然大面积超时,用户疯狂刷新,页面全白。我查了半天,发现是调用一个第三方风控服务时,对方服务响应变慢,而我们的 HTTP 客户端没有设超时,线程池被这些“永远等不到响应”的请求占满,连锁反应拖垮了整个下单链路。最后运维手动重启了服务节点才恢复,但那 20 分钟里,订单损失六位数。
一、不设超时,等于把命交给别人
那次事故让我明白一个道理:你调用的每一个外部服务,都可能成为你的致命弱点。 不设超时,就等于告诉系统:“你可以无限期等下去。”而线程池、连接池都是有限的,一个慢请求就能占住一个线程,堆积起来就是雪崩。
我后来统计了一下,我们系统里调用的外部服务有 17 个:支付、风控、短信、邮件、地图、推荐、用户画像……任何一个出问题,如果没有超时保护,都会直接拖垮我们。
二、超时要配几层?我踩过的坑告诉你
1. 连接超时 vs 读取超时,别搞混
很多人只配一个 timeout,其实要分两层:
- 连接超时(Connect Timeout):建立 TCP 连接的最长时间,一般设 1-3 秒。连都连不上,说明网络或服务有问题,赶紧失败。
- 读取超时(Read Timeout):连接建立后,等待数据返回的最长时间,一般设 3-10 秒。连上了但一直不返回,说明服务处理慢,也要及时断开。
// HttpClient 示例
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(2000) // 2秒连不上就放弃
.setSocketTimeout(5000) // 5秒读不到数据就放弃
.build();
2. 连接池超时,很多人忘了
如果用连接池(比如 PoolingHttpClientConnectionManager),还要配从池里获取连接的超时:
.setConnectionRequestTimeout(1000) // 1秒拿不到连接就放弃
不然连接池满了,请求会阻塞在这里,效果和不设超时一样。
3. 框架层面的超时
我们用 Spring Cloud OpenFeign,除了 HTTP 客户端本身的超时,还要配 Ribbon 或 LoadBalancer 的超时:
feign:
client:
config:
default:
connectTimeout: 2000
readTimeout: 5000
而且要注意:Feign 的超时和底层 HttpClient 的超时,取的是最小值。配错了可能你以为设了 5 秒,实际 1 秒就断了。
三、超时设多少?不是拍脑袋
我以前随便写个 5000、10000,后来发现这也不对。超时时间要根据业务场景和P99 响应时间来定:
| 场景 | P99 耗时 | 建议超时 | 原因 |
|---|---|---|---|
| 内部服务调用(同机房) | 50ms | 500ms | 内网快,超 500ms 大概率是挂了 |
| 同区域云服务 | 100ms | 1s | 网络稳定,1s 足够 |
| 第三方 API(支付) | 800ms | 3s | 跨网络,留余量 |
| 第三方 API(风控) | 200ms | 2s | 风控要快,但不能太短 |
| 文件上传/下载 | 不定 | 30s | 大文件要留够时间 |
原则:超时时间 = P99 耗时 × 3,但不超过用户能接受的最大等待时间。如果超时太短,正常请求也会被截断;太长,又起不到保护作用。
四、超时之后怎么办?不是抛异常就完了
超时只是第一步,关键是超时后的处理策略:
1. 快速失败 + 降级
非核心服务超时,直接降级,别影响主流程:
try {
riskResult = riskService.check(order);
} catch (TimeoutException e) {
log.warn("risk service timeout, skip risk check, orderId={}", orderId);
riskResult = RiskResult.PASS; // 降级为放行,事后异步补检
}
2. 重试 + 退避
幂等接口可以重试,但要加退避:
@Retryable(value = TimeoutException.class, maxAttempts = 3, backoff = @Backoff(delay = 500))
public Result callExternal() { ... }
注意:重试只对幂等接口有意义,非幂等接口重试会出大事(参见我之前写的幂等那篇)。
3. 熔断:别让超时请求堆积
如果某个服务持续超时,说明它已经不健康了,继续调只会浪费资源。用熔断器(Resilience4j 或 Sentinel)自动断开:
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 失败率超 50% 打开熔断
.waitDurationInOpenState(Duration.ofSeconds(30)) // 30 秒后半开
.build();
五、一个容易忽略的点:数据库也有超时
HTTP 调用要超时,数据库操作也要。我们之前有个慢查询,执行了 120 秒,连接一直占着,连接池耗尽。后来给 MyBatis 的 SQL 执行加了超时:
@Options(timeout = 3) // 3秒超时
List<Order> selectLargeList(@Param("userId") Long userId);
Spring 的 @Transactional 也可以设超时:
@Transactional(timeout = 5) // 5秒
public void createOrder(Order order) { ... }
六、一个心态的转变
以前我觉得超时配置是“细节”,现在觉得是系统稳定性的第一道防线。你不设超时,系统就没有“刹车”——一个慢请求就能把整条链路拖死。设了超时,至少你知道最坏情况下系统会在多久内释放资源,而不是无限期等下去。
如果你现在的代码里还有没配超时的外部调用,今晚就补上。不用纠结具体数字,先设一个保守值(比如 5 秒),上线观察,再根据实际 P99 调整。每补一个超时配置,你的系统就多一分韧性,你睡觉就多一分踏实。毕竟,周五下午的告警电话,谁都不想接。
- 点赞
- 收藏
- 关注作者
评论(0)