超时类型:连接超时、读取超时、写入超时、空闲超时

老实说,我在处理超时问题时,发现很多同学搞不清这几种超时的区别。甚至有人认为“超时就是连不上”。嗯,这理解太粗略了。

咱们把超时做个拆解。一个完整的网络请求,说白了就是“发起连接 → 发送数据 → 等待响应 → 接收数据”这么个链条。每一步都可能卡住,对应的超时策略也不同。

一句话总结:连接超时管“能不能连上”,读取超时管“对方有没有回”,写入超时管“数据能不能发出去”,空闲超时管“连接能不能复用”。

连接超时(Connection Timeout)

连接超时,最常见,也最好理解。它控制的是 TCP 三次握手阶段的最长等待时间。

你想想看,客户端发起 SYN,等着服务端回 SYN-ACK。如果网络不通、IP 不存在、端口没开,或者防火墙直接把包丢了,这个握手过程就会一直挂在那。

我个人的习惯是:连接超时一定要比服务端正常响应时间短。比如你的服务物理延迟是 50ms,那连接超时设成 5s 就够了。我曾经接手过一个项目,连接超时设了 120s,结果服务器挂了之后,所有请求都在那干等两分钟才报错,线上故障恢复时间硬生生被拖长了一倍。

import socket

# 设置连接超时
socket.setdefaulttimeout(5)  # 全局5秒

# 或者用 requests 库
import requests
try:
    resp = requests.get('https://api.example.com', timeout=5)
except requests.exceptions.ConnectTimeout:
    print('连接超时,请检查网络或服务端')
注意:连接超时设得太短也不行。如果服务偶尔突发延迟,设成 1s 可能频繁误报。建议根据历史 P99 延迟来定,一般 3~10s 是比较合理的范围。

读取超时(Read / Socket Timeout)

连接建立成功了,数据发过去了,然后呢?客户端开始等响应。这个等待响应的时间,就是读取超时。

我在项目中遇到过一个案例:服务端因为数据库慢查询,某个接口要跑 15s 才能返回。而客户端读取超时只设了 10s。结果每次调用都触发 ReadTimeout,客户端不断重试,服务端压力雪上加霜,最后整条链路过载。排查过程相当痛苦。

读取超时跟连接超时不一样。连接超时是“出门找人”,找不到就放弃;读取超时是“人找到了,但对方迟迟不开口”。

# requests 中单位是秒,设成 (连接超时, 读取超时)
resp = requests.get('https://api.example.com', timeout=(3, 10))
# 或者简写,两个都设成同一个值
resp = requests.get('https://api.example.com', timeout=10)
超时类型 触发时机 常见原因 推荐设置
连接超时 TCP 握手阶段 网络不通、IP 错误、防火墙拦截 3~10s
读取超时 发送请求后等待响应 慢查询、死锁、上游服务超时 10~60s(根据业务调整)

写入超时(Write Timeout)

读取超时大家很熟悉,但写入超时往往被忽略。我刚开始写 SDK 的时候也犯过这个错——以为发送数据是瞬间完成的。

实际上,如果服务端的接收缓冲区满了,或者 TCP 窗口为 0,发送端就会阻塞。这种场景在大量并发推送、上传大文件时尤其常见。

写入超时控制的是从“发送方开始发数据”到“数据被对端确认收到”之间的时间。

典型场景:用 HTTP/2 做推送,同时给 10 万个客户端发消息。如果某个客户端网络极烂,TCP 滑动窗口卡死,没有写入超时保护,整个 EventLoop 都被它拖住。

# Python 中手动给 socket 设置写入超时
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)  # 这个 timeout 同时影响读取和写入
# 如果想单独控制写入超时,可以用 setblocking + select
sock.setblocking(0)
ready = select.select([], [sock], [], 5)  # 只检查可写
if not ready[1]:
    raise TimeoutError('写超时')

空闲超时(Idle Timeout)

空闲超时跟前三个都不太一样。它不属于单次请求,而是管理连接的“保鲜期”。

连接池里的连接,如果长时间没人用,服务端可能把它关了(或者 NAT 网关把它释放了)。如果客户端不知道,下次复用这条连接,就会收到一个“惊悚”的异常——Connection Reset 或者 Broken Pipe。

我记得有一次线上事故,就是因为连接池空闲超时设了 600s,而中间防火墙空闲断开策略是 120s。结果每过一段时间,就有一批请求在复用连接时瞬间断开,业务报错像瀑布一样涌出来。

一般建议:

  • 连接池的空闲超时 小于 网络中间件的断开时间(通常 60s~300s)
  • 配合 心跳检测 或 Keep-Alive 来保活
  • 在复用前做一次 连接健康检查(比如发一个轻量的 PING)
# 以 redis 连接池为例
from redis import ConnectionPool

pool = ConnectionPool(
    max_connections=10,
    socket_connect_timeout=5,
    socket_timeout=5,          # 读取超时
    socket_keepalive=True,     # 开启 TCP Keep-Alive
    retry_on_timeout=True,
    # 某些 Redis 客户端支持设置空闲连接检查间隔
    health_check_interval=30,  # 每 30s 检查一次空闲连接是否有效
)
避坑:不要把空闲超时设成 0 或负数。有些客户端默认值是 0,表示“从不清理空闲连接”,这反而更容易导致连接泄漏。我曾经在一个高并发服务里这样做,最后连接数飙到了几万,服务直接被 OOM Killed。

一张图理解四种超时

讲到这里,你可能觉得这四种超时有点绕。我画了一张流程图,帮你理清它们在请求生命周期里的位置和作用。

超时类型在请求生命周期中的位置 请求生命周期中的超时类型 客户端发起 连接超时 TCP 握手阶段 服务端接收 客户端接收 写入超时 数据发送阻塞 / 窗口满 读取超时 等待响应超时 空闲超时 连接复用前的“保鲜期” 📌 连接超时 → 读取超时 → 写入超时 → 空闲超时, 它们分别拦截请求生命中不同阶段的异常 一个请求经过:连接→发送→等待→接收

看到这张图就清楚了:连接超时卡在开头,写入超时卡在发送途中,读取超时卡在等待回包,空闲超时管的是多个请求之间的连接复用。

实战建议:

  • 在一个 SDK 里,把四种超时都暴露为可配置的参数,不要写死
  • 给每个超时设置一个合理的默认值,并且在报错信息里明确指出是什么超时
  • 日志里一定要打印出超时类型、超时阈值、实际等待时间,方便排查是设得太短还是服务真的慢了

到这步,你已经能分清这四种超时了。下一步就是针对每种超时,设计对应的重试策略。这个咱们后面再聊。