常见超时类型:连接超时、读取超时、写入超时
做接口超时排查,第一个要搞清楚的,就是——到底超时在哪个阶段?
我见过太多人一上来就调大超时参数,结果问题根本不在那里。你想想看,连 TCP 握手都没完成,你改读取超时有什么用?
所以这一章,咱们把三种最基础也最常见的超时类型掰开揉碎聊一遍:连接超时、读取超时、写入超时。
连接超时(Connect Timeout)
定义:从客户端发起 TCP 三次握手,到握手完成(或拒绝)所允许的最大等待时间。
说白了,就是双方能不能搭上线。你打电话对方一直不接,你总不能一直等下去吧?
典型的根因:
- 网络不通(防火墙、安全组、路由不可达)
- 服务端端口未监听(进程挂了或端口配错)
- 服务端队列满(syn backlog 溢出)
- 客户端连接池已耗尽,新请求排队等连接
核心判断方法:
用 telnet IP PORT 或 nc -vz IP PORT 测试,3 秒内如果能连上就正常。连不上就是连接超时。
我个人习惯:调连接超时不要设太长。内网一般 1~3 秒,跨机房 3~5 秒,公网不超过 10 秒。太长反而会让整个系统瘫痪——你想想看,所有线程都在傻等连接,其他请求就进不来了。
避坑指南:我曾经遇到一个诡异案例,连接超时偶尔报错,但 telnet 每次都通。后来抓包发现是 TCP 快速重传被中间设备丢弃,导致 SYN 无法完成握手。嗯,这种问题常规手段很难查,得结合 tcpdump 看交互。
# 快速验证连接是否可达
telnet 10.0.0.1 8080
# 如果超时,检查端口监听
ss -tlnp | grep 8080 # 或 netstat -anp
# 抓包看三次握手
tcpdump -i eth0 host 10.0.0.1 and port 8080 -w connect.pcap
读取超时(Read Timeout / Socket Timeout)
定义:客户端已经成功建立连接,并且已经发送完请求,但在等待服务端返回响应的过程中,连续两次数据到达之间超过设定阈值。
我经常跟团队说:读取超时是最「冤」的,因为很多时候它只是服务端处理慢,并不是真的断了。但作为客户端,你不能无限等下去对吧?
常见的触发场景:
- 服务端处理请求太慢(数据库慢查询、下游调用阻塞、CPU 繁忙)
- 服务端已经崩溃或挂起(进程活但无法响应)
- 网络丢包导致客户端收不到最后几个包(但 TCP 仍存活)
- 客户端代码自身未及时读取流(缓冲区满导致阻塞)
注意:读取超时 ≠ 连接断开。
读取超时后,socket 会被 RFC 视为“状态不明”,不能直接复用。你如果不关连接就重用,下一个请求可能读到上一轮的残留数据或直接抛出异常。所以大多数 HTTP 库在读取超时后会主动关闭连接。
我的排查习惯:先看服务端日志,确认请求是否被接收并开始处理。如果服务端日志显示已返回,客户端却读超时——那就大概率是网络问题,用 tcpdump 看重组后的响应包是否完整。
# Java 中使用 Apache HttpClient 设置读取超时
RequestConfig reqConfig = RequestConfig.custom()
.setConnectionRequestTimeout(2000) // 从连接池获取连接的超时
.setConnectTimeout(3000) // 连接建立超时
.setSocketTimeout(5000) // 读取超时 —— 就是这个参数
.build();
你想想看:如果一个接口平均响应 200ms,读取超时设 5 秒是不是很合理?不一定。要是服务端偶发全 GC 暂停 8 秒,5 秒超时就频繁报了。这时候要结合 p99 延迟来设,至少覆盖 99% 的正常场景,不能只看平均值。
避坑指南:我曾经调过一个金融系统的超时参数,开发把读取超时设成 60 秒,觉得大一点安全。结果全链路雪崩——上游超时 60 秒堆满线程,下游根本没机会恢复。记住:读取超时不是越大越好,它是系统容错里的一道防线。
写入超时(Write Timeout)
定义:向已建立的 TCP 连接写入数据时,数据无法在指定时间内成功发送到底层缓冲区的全部(或部分)所抛出的超时。
写入超时比前两种少见,但一旦出现,往往和网络拥堵或连接异常强相关。
典型原因:
- 服务端接收窗口为 0(TCP 流控导致发送端阻塞)
- 连接已被服务端 RST 或 FIN 关闭(半开连接)
- 本地发送缓冲区已满(例如消息体极大)
- 操作系统内核参数限制(
tcp_wmem太小)
关键区别:连接超时是「连不上」,写入超时是「连上了,但写不进去」。后者通常意味着服务端不再接收数据,或者链路已坏。
排查思路:先检查服务端是否正常在处理请求。如果服务端正常,大概率是网络中间设备丢弃了数据包,或者客户端与服务器之间的 TCP 窗口问题。可以用 ss -ti 查看 socket 的 rcv_wnd / snd_wnd。
# 查看 TCP socket 窗口大小
ss -ti | grep -A1 "10.0.0.1:8080"
我建议:写入超时在大多数业务场景下可以和读取超时合并成一个参数「socket timeout」。但在文件上传、大消息推送等场景里,必须单独配置写入超时,因为写大 payload 需要更长时间。
三种超时的速查对比
| 类型 | 本质 | 典型阈值 | 最常用的排查命令 |
|---|---|---|---|
| 连接超时 | TCP 握手未完成 | 内网 2s,公网 5~10s | telnet / nc |
| 读取超时 | 等待响应包间隔超时 | 接口 p99 * 2~3 | 服务端耗时日志 + tcpdump |
| 写入超时 | 发送数据阻塞 | 一般与读取超时相同 | ss -ti 看窗口 |
一个隐藏陷阱:连接复用下的超时误判
现在 HTTP/1.1 和连接池很普遍。如果你复用一个已有的长连接,下一次请求不需要再经过连接超时阶段。但连接可能已经悄悄断了(比如空闲超时被中间设备杀掉)。
这时候你发的请求会直接报「读取超时」或「写入超时」,你查半天发现根本不是服务端慢,而是连接早就死了。嗯,这里要注意:读超时不一定真是读慢,也可能是连接被复用了一个死连接。
避坑指南:我曾经因为 OKHttp 默认连接池 keep-alive 5 分钟,但 Nginx 空闲超时设了 60 秒。结果很多请求走长连接时等待 60 秒才超时重试。最后把客户端 keep-alive 改成 30 秒就解决了。所以,客户端和服务端的连接超时参数要保持一致。
三种超时,看起来简单,但踩坑的人一茬接一茬。搞清楚了它们各自的位置和含义,排查超时问题才不至于大海捞针。