第97章 超时模式(Timeout):连接超时、读取超时、超时重试,以及OkHttp与Feign超时配置
说起超时,我脑子里立刻浮现出几年前的一个深夜。
那会儿我们系统突然大面积卡死,所有请求都像石沉大海。排查了半天,发现是下游的一个服务挂了,但我们的HTTP客户端还在傻傻地等——默认超时是0,也就是永不超时。结果线程池被活活撑爆,整个应用挂了。
嗯,从那以后,我对超时配置就特别敏感。
什么是超时模式?
说白了,超时模式就是给每个操作设定一个「等待上限」。超过这个时间,就别等了,赶紧报错或者走备选方案。
你想想看,一个请求发出去,对方要是永远不响应,你的线程就得一直挂着。这在单线程时代可能还能忍,但在高并发场景下,线程就是最宝贵的资源,浪费不起。
超时模式主要解决三个问题:
- 资源保护:防止线程被无限期阻塞
- 故障隔离:某个服务挂了,不影响调用方
- 用户体验:用户不需要等太久,快速失败比慢速成功更好
超时的三种类型
我在项目中见过很多人只配一个超时时间,其实这是不够的。一个完整的HTTP请求,至少涉及三个阶段的超时:
| 超时类型 | 含义 | 典型值 | 我踩过的坑 |
|---|---|---|---|
| 连接超时 | 建立TCP连接的最大等待时间 | 500ms ~ 2s | 设太短,网络抖动就失败 |
| 读取超时 | 从连接读取数据的最大间隔时间 | 5s ~ 30s | 设太长,慢接口拖垮线程池 |
| 写入超时 | 发送请求数据的最大等待时间 | 1s ~ 5s | 一般很少单独配 |
核心原则:连接超时要短,读取超时要根据业务定。连接超时是网络层面的,通常几百毫秒就够了;读取超时是业务层面的,得看接口的P99响应时间。
超时重试:别让失败变成雪崩
超时之后怎么办?直接抛异常?还是再试一次?
我个人习惯是:幂等操作可以重试,非幂等操作坚决不重试。
比如查询接口,超时了重试一次没问题。但如果是下单、转账这种操作,重试可能导致重复扣款——那麻烦就大了。
重试策略一般有三种:
- 固定间隔重试:等1秒再试,简单粗暴
- 指数退避:第一次等1秒,第二次等2秒,第三次等4秒……防止打爆下游
- 抖动退避:在指数退避基础上加随机值,避免所有客户端同时重试
我的经验:重试次数不要超过3次。我曾经见过一个系统配了10次重试,结果下游挂了之后,上游自己先被重试流量打死了。
Java中的超时实现
先看一个最基础的例子。用Java原生Socket实现超时:
// 连接超时 + 读取超时
Socket socket = new Socket();
socket.connect(new InetSocketAddress("example.com", 80), 2000); // 2秒连接超时
socket.setSoTimeout(5000); // 5秒读取超时
// 或者用URLConnection
URL url = new URL("http://example.com");
URLConnection conn = url.openConnection();
conn.setConnectTimeout(2000);
conn.setReadTimeout(5000);
嗯,这里要注意:setSoTimeout 是读取超时,不是总超时。很多人搞混过,包括我。
OkHttp的超时配置
OkHttp是我最常用的HTTP客户端。它的超时配置非常清晰:
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(2, TimeUnit.SECONDS) // 连接超时
.readTimeout(10, TimeUnit.SECONDS) // 读取超时
.writeTimeout(5, TimeUnit.SECONDS) // 写入超时
.build();
Request request = new Request.Builder()
.url("http://example.com/api")
.build();
// 同步调用
try (Response response = client.newCall(request).execute()) {
// 处理响应
} catch (SocketTimeoutException e) {
// 超时处理
System.err.println("请求超时了:" + e.getMessage());
}
注意:OkHttp的超时是针对单个请求的。如果你用同一个client并发发100个请求,每个请求的超时是独立的,不会互相影响。
OkHttp还支持重试。默认情况下,如果发生路由错误或连接失败,OkHttp会自动重试。但超时导致的异常不会自动重试——这个设计我觉得挺合理的,超时说明服务端可能已经处理了,重试反而危险。
Feign的超时配置
Feign在微服务里用得很多。它底层可以用OkHttp或者Apache HttpClient。配置方式有两种:
方式一:全局配置
# application.yml
feign:
client:
config:
default:
connectTimeout: 2000
readTimeout: 5000
loggerLevel: basic
方式二:针对某个服务单独配置
feign:
client:
config:
user-service: # 服务名
connectTimeout: 1000
readTimeout: 3000
如果Feign底层用的是Ribbon做负载均衡,还要注意Ribbon也有自己的超时配置:
ribbon:
ConnectTimeout: 2000
ReadTimeout: 5000
避坑指南:我曾经遇到过一个诡异的问题——Feign配置了5秒超时,但请求总是10秒才超时。查了半天才发现,Ribbon的超时默认是10秒,而且Feign的超时和Ribbon的超时是「取最大值」的关系。所以一定要把两者配成一致,或者干脆只用Feign的超时。
超时模式的核心流程图
下面这张图是我自己总结的超时处理流程,每次设计新系统我都会拿出来参考:
超时配置的最佳实践
说了这么多,到底该怎么配?我总结了几条经验:
- 连接超时不要超过2秒。正常的内网调用,几百毫秒就够了。外网调用可以放宽到2秒。
- 读取超时根据P99来定。比如接口的P99是3秒,那读取超时设5秒就够。别设成30秒,那等于没设。
- 重试要加退避。别一超时就立刻重试,大概率还是超时。等一等再试,成功率更高。
- 区分读操作和写操作。读操作可以重试,写操作要谨慎。实在要重试,得保证幂等。
- 监控超时率。如果超时率突然飙升,说明下游可能出问题了,这时候应该降级而不是重试。
一个小技巧:我习惯在配置中心里动态管理超时时间。这样线上出问题时,可以秒级调整,不用重启应用。比如用Apollo或者Nacos,把超时时间配成可动态刷新的配置项。
总结
超时模式看起来简单,但坑不少。连接超时、读取超时、重试策略,每一个都值得认真对待。
我记得有一次,一个同事把读取超时设成了1毫秒,结果所有请求都超时了。排查了半天才发现是配置写错了。嗯,这种低级错误我也犯过,所以现在每次改超时配置,我都会再三检查。
最后送大家一句话:超时不是银弹,但没有超时,系统就是纸糊的。
公众号:蓝海资料掘金营,微信deep3321