在开发接口调用程序时,有一种故障比较难定位:接口大部分时间都能正常返回,但运行一段时间后,偶尔会出现连接中断、读取超时或者远端关闭连接的情况。手动重试一次,接口又恢复正常了。
如果只是偶发一两次,开发人员可能会先加一层重试逻辑。但当请求量增加,或者程序需要长期运行时,这种问题就可能变成持续出现的线上故障。
这类问题不一定是服务端处理能力不足,也不一定是网络带宽不够。HTTP 连接复用、连接池管理、超时配置以及中间网络设备的连接回收,都可能影响请求的稳定性。
本文使用 Python Requests 和 Linux 常见排障命令,梳理一套从应用层到 TCP 层的排查思路。
一、为什么重试一次就恢复正常?
先看一个常见的请求方式:
import requests
url = "https://httpbin.org/get"
response = requests.get(url, timeout=10)
print(response.status_code)
print(response.text)
这种写法适合简单的独立请求。但如果程序需要连续调用同一个 API,连接建立和释放的过程就值得关注。
一次 HTTPS 请求通常涉及以下步骤:
-
DNS 解析,获取目标服务器地址。
-
建立 TCP 连接。
-
完成 TLS 握手。
-
发送 HTTP 请求并等待响应。
-
接收响应数据,决定连接是否继续复用。
如果每次请求都新建连接,重复的 TCP 和 TLS 握手会带来额外开销。因此,HTTP 客户端通常会通过连接池复用符合条件的连接。
问题也可能出现在这里。
假设客户端完成一次请求后,将 TCP 连接放回连接池。随后,服务端、负载均衡器或代理设备因为空闲超时等原因关闭了这条连接,而客户端尚未及时感知。
当下一次请求尝试复用这条连接时,就可能发生连接重置、远端关闭连接等异常。重新建立连接后,请求又恢复正常。
这只是其中一种可能的故障机制,并非所有偶发错误都由连接池引起。DNS 异常、网络丢包、服务端限流和 TLS 错误也需要分别排查。
二、先从 Python 客户端检查连接池与超时
对于需要连续调用 API 的程序,可以使用 requests.Session() 管理会话,并通过 HTTPAdapter 配置连接池。
import requests
from requests.adapters import HTTPAdapter
session = requests.Session()
adapter = HTTPAdapter(
pool_connections=10,
pool_maxsize=20,
max_retries=0
)
session.mount("https://", adapter)
session.mount("http://", adapter)
response = session.get(
"https://httpbin.org/get",
timeout=(3.05, 10)
)
print(response.status_code)
print(response.text[:200])
这里有几个参数需要区分:
-
pool_connections:连接池缓存的主机连接池数量。 -
pool_maxsize:每个连接池可保留的连接数量上限。 -
max_retries:底层适配器的重试配置。 -
timeout=(3.05, 10):分别设置连接超时和读取超时。
需要注意,pool_maxsize 并不总是等于严格的最大并发连接数。在默认 pool_block=False 的情况下,超出池大小的并发请求仍可能临时创建额外连接;如果设置 pool_block=True,则需要进一步考虑等待可用连接的时间。
另外,Requests 的读取超时主要限制等待响应数据时允许的空闲时间,并不等于整个请求从开始到结束的绝对时间上限。
不要一发现超时就直接调大所有参数。 如果问题来自连接池中的空闲连接失效,盲目增加池大小不一定有帮助;如果是服务端响应慢,单纯增加连接数量也可能加重服务端压力。
排查时可以先记录请求开始时间、异常类型、目标主机、响应状态码和重试次数,再结合实际故障频率调整参数。
三、用 curl 拆分请求各阶段的耗时
当程序出现偶发超时,首先要确认问题发生在哪个阶段。
可以使用 curl 查看 DNS、TCP、TLS、首字节响应和完整请求耗时:
curl -sS -o /dev/null \
--connect-timeout 3 \
--max-time 15 \
-w 'dns=%{time_namelookup}s\nconnect=%{time_connect}s\ntls=%{time_appconnect}s\nttfb=%{time_starttransfer}s\ntotal=%{time_total}s\n' \
https://httpbin.org/get
输出结果可以帮助我们初步判断问题:
| 指标 | 主要含义 | 异常时的排查方向 |
|---|---|---|
time_namelookup |
DNS 解析耗时 | DNS 配置、解析链路 |
time_connect |
TCP 建连耗时 | 路由、丢包、连接建立 |
time_appconnect |
TLS 握手完成耗时 | TLS 协商、证书及中间链路 |
time_starttransfer |
收到首字节的总耗时 | 服务端处理、上游等待 |
time_total |
整个请求耗时 | 响应体大小、传输速度等 |
需要特别注意,这些时间通常是从请求开始累计计算的,而不是每个阶段单独消耗的时间。
例如,time_starttransfer 包含前面已经经历的 DNS、TCP 和 TLS 等阶段。想分析服务端等待时间,可以结合相邻指标作差,但要考虑重定向、代理和连接复用等情况。
此外,上面的 curl 命令主要用于观察单次请求。它默认不能直接复现 Python Session 中的连接池复用问题。如果故障只在长时间运行的程序中出现,还需要在客户端循环调用,并记录每次请求的耗时与异常。
四、从 Linux TCP 状态判断是否存在连接异常
如果应用日志显示连接中断,但暂时无法判断是客户端还是网络链路的问题,可以在 Linux 环境下观察 TCP 连接状态。
先统计连接状态:
ss -tan | awk 'NR > 1 {count[$1]++} END {for (s in count) print s, count[s]}'
也可以分别检查常见状态:
ss -tan state established
ss -tan state time-wait
ss -tan state close-wait
ss -tan state syn-sent
这些状态有不同的含义:
ESTABLISHED
表示 TCP 连接已经建立。连接数量多并不一定是故障,还需要结合应用并发量和连接持续时间判断。
TIME_WAIT
通常与主动关闭 TCP 连接后的状态管理有关。高频短连接可能产生大量 TIME_WAIT,但不能仅凭数量就认定端口耗尽。
CLOSE_WAIT
表示本机 TCP 栈已经收到对端的关闭通知,但本地应用尚未完成关闭流程。如果该状态持续累积,需要检查应用是否及时释放 Socket、响应对象或其他连接资源。
SYN_SENT
表示连接请求已经发出,但握手尚未完成。需要继续检查目标地址、路由、防火墙策略和丢包情况。
还要注意,ss 展示的是执行命令时的连接状态快照。对于只出现几百毫秒的间歇性故障,单次检查可能什么也看不到。可以在故障发生期间持续采样,并将采样时间与应用日志对齐。
五、怎么判断是连接池、服务端还是网络链路的问题?
排查时不建议只看一个错误提示,可以按照下面的顺序缩小范围。
第一步:记录准确的异常类型。
例如 ConnectTimeout、ReadTimeout、ConnectionResetError 和 RemoteDisconnected 对应的故障阶段并不相同。先区分建连失败、读取超时和连接被关闭,再决定下一步。
第二步:比较单次请求和连续请求。
如果单次 curl 测试一直正常,但长期运行的 Session 偶尔失败,可以重点检查连接复用、空闲连接超时及连接池使用方式。不过,这还不足以直接证明连接池就是根因。
第三步:对比不同网络路径。
在授权且配置一致的情况下,可以比较不同客户端或不同出口的结果。如果只有某条路径出现异常,应进一步检查该路径中的 DNS、路由、代理以及中间设备,而不是直接认定目标服务器有问题。
第四步:检查服务端和中间设备日志。
对照客户端请求时间,查看负载均衡器、反向代理及目标服务的访问日志。如果客户端报错时服务端完全没有收到请求,问题可能发生在请求到达服务端之前;如果服务端已经收到请求,则需要继续检查处理耗时和响应返回链路。
需要注意,客户端日志缺失并不能单独证明请求没有到达服务器。日志采样、缓冲、时钟偏差以及中间代理行为都可能影响判断。
六、生产环境中应该怎样减少这类问题?
排查完成后,可以从以下几个方面改进客户端。
-
为外部请求设置明确的超时。 区分连接超时和读取超时,避免请求无限期占用工作线程。
-
合理使用连接池。 根据并发量、请求频率和目标服务的连接限制调整池参数,而不是单纯追求更大的数值。
-
谨慎设计重试。 只对适合重试的错误进行有限次数重试,并采用退避策略。对于可能产生重复副作用的写入操作,需要先确认接口是否支持幂等处理。
-
记录关键诊断信息。 至少保留请求时间、目标主机、耗时、异常类型、状态码和重试次数,必要时记录连接复用相关信息。
-
不要忽略网络出口。 如果请求经过代理、网关或负载均衡器,还要核对连接空闲超时、认证状态、路由路径以及出口是否发生变化。
网络出口只是整个请求链路中的一个环节。即使出口地址固定,也不能排除 DNS、TCP、TLS、服务端处理和连接池配置导致的故障。
总结
API 偶尔失败、重试后恢复,并不意味着必须更换网络或者增加服务器资源。
更有效的做法,是先确定错误发生在哪个阶段,再结合 HTTP 客户端日志、curl 耗时、TCP 状态和服务端记录逐步定位。
如果问题集中在长时间运行的连接池,重点检查连接复用与空闲连接管理;如果 TCP 建连阶段耗时异常,则需要进一步检查路由、丢包和网络出口;如果连接已经建立但响应迟迟没有返回,则还需要分析服务端处理和上游依赖。
排障的核心不是让请求不断重试,而是让每次失败都能提供足够的信息,帮助我们缩小问题范围。