超时类型:连接超时、读取超时、写入超时、空闲超时
老实说,我在处理超时问题时,发现很多同学搞不清这几种超时的区别。甚至有人认为“超时就是连不上”。嗯,这理解太粗略了。
咱们把超时做个拆解。一个完整的网络请求,说白了就是“发起连接 → 发送数据 → 等待响应 → 接收数据”这么个链条。每一步都可能卡住,对应的超时策略也不同。
连接超时(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('连接超时,请检查网络或服务端')
读取超时(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 检查一次空闲连接是否有效
)
一张图理解四种超时
讲到这里,你可能觉得这四种超时有点绕。我画了一张流程图,帮你理清它们在请求生命周期里的位置和作用。
看到这张图就清楚了:连接超时卡在开头,写入超时卡在发送途中,读取超时卡在等待回包,空闲超时管的是多个请求之间的连接复用。
实战建议:
- 在一个 SDK 里,把四种超时都暴露为可配置的参数,不要写死
- 给每个超时设置一个合理的默认值,并且在报错信息里明确指出是什么超时
- 日志里一定要打印出超时类型、超时阈值、实际等待时间,方便排查是设得太短还是服务真的慢了
到这步,你已经能分清这四种超时了。下一步就是针对每种超时,设计对应的重试策略。这个咱们后面再聊。