常见超时类型:连接超时、读取超时、写入超时

做接口超时排查,第一个要搞清楚的,就是——到底超时在哪个阶段?

我见过太多人一上来就调大超时参数,结果问题根本不在那里。你想想看,连 TCP 握手都没完成,你改读取超时有什么用?

所以这一章,咱们把三种最基础也最常见的超时类型掰开揉碎聊一遍:连接超时、读取超时、写入超时。

一次 HTTP 请求的生命周期 —— 超时发生位置 客户端 服务端 ① 连接超时 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)

定义:客户端已经成功建立连接,并且已经发送完请求,但在等待服务端返回响应的过程中,连续两次数据到达之间超过设定阈值。

我经常跟团队说:读取超时是最「冤」的,因为很多时候它只是服务端处理慢,并不是真的断了。但作为客户端,你不能无限等下去对吧?

常见的触发场景:

  1. 服务端处理请求太慢(数据库慢查询、下游调用阻塞、CPU 繁忙)
  2. 服务端已经崩溃或挂起(进程活但无法响应)
  3. 网络丢包导致客户端收不到最后几个包(但 TCP 仍存活)
  4. 客户端代码自身未及时读取流(缓冲区满导致阻塞)

注意:读取超时 ≠ 连接断开。

读取超时后,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~10stelnet / nc
读取超时等待响应包间隔超时接口 p99 * 2~3服务端耗时日志 + tcpdump
写入超时发送数据阻塞一般与读取超时相同ss -ti 看窗口

一个隐藏陷阱:连接复用下的超时误判

现在 HTTP/1.1 和连接池很普遍。如果你复用一个已有的长连接,下一次请求不需要再经过连接超时阶段。但连接可能已经悄悄断了(比如空闲超时被中间设备杀掉)。

这时候你发的请求会直接报「读取超时」或「写入超时」,你查半天发现根本不是服务端慢,而是连接早就死了。嗯,这里要注意:读超时不一定真是读慢,也可能是连接被复用了一个死连接。

避坑指南:我曾经因为 OKHttp 默认连接池 keep-alive 5 分钟,但 Nginx 空闲超时设了 60 秒。结果很多请求走长连接时等待 60 秒才超时重试。最后把客户端 keep-alive 改成 30 秒就解决了。所以,客户端和服务端的连接超时参数要保持一致。

三种超时,看起来简单,但踩坑的人一茬接一茬。搞清楚了它们各自的位置和含义,排查超时问题才不至于大海捞针。