为什么 API 接口偶尔报错,重试一次又恢复了?从 HTTP 连接池到 Keep-Alive 排查

在开发接口调用程序时,有一种故障比较难定位:接口大部分时间都能正常返回,但运行一段时间后,偶尔会出现连接中断、读取超时或者远端关闭连接的情况。手动重试一次,接口又恢复正常了。

如果只是偶发一两次,开发人员可能会先加一层重试逻辑。但当请求量增加,或者程序需要长期运行时,这种问题就可能变成持续出现的线上故障。

这类问题不一定是服务端处理能力不足,也不一定是网络带宽不够。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 请求通常涉及以下步骤:

  1. DNS 解析,获取目标服务器地址。

  2. 建立 TCP 连接。

  3. 完成 TLS 握手。

  4. 发送 HTTP 请求并等待响应。

  5. 接收响应数据,决定连接是否继续复用。

如果每次请求都新建连接,重复的 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、路由、代理以及中间设备,而不是直接认定目标服务器有问题。

第四步:检查服务端和中间设备日志。

对照客户端请求时间,查看负载均衡器、反向代理及目标服务的访问日志。如果客户端报错时服务端完全没有收到请求,问题可能发生在请求到达服务端之前;如果服务端已经收到请求,则需要继续检查处理耗时和响应返回链路。

需要注意,客户端日志缺失并不能单独证明请求没有到达服务器。日志采样、缓冲、时钟偏差以及中间代理行为都可能影响判断。

六、生产环境中应该怎样减少这类问题?

排查完成后,可以从以下几个方面改进客户端。

  1. 为外部请求设置明确的超时。 区分连接超时和读取超时,避免请求无限期占用工作线程。

  2. 合理使用连接池。 根据并发量、请求频率和目标服务的连接限制调整池参数,而不是单纯追求更大的数值。

  3. 谨慎设计重试。 只对适合重试的错误进行有限次数重试,并采用退避策略。对于可能产生重复副作用的写入操作,需要先确认接口是否支持幂等处理。

  4. 记录关键诊断信息。 至少保留请求时间、目标主机、耗时、异常类型、状态码和重试次数,必要时记录连接复用相关信息。

  5. 不要忽略网络出口。 如果请求经过代理、网关或负载均衡器,还要核对连接空闲超时、认证状态、路由路径以及出口是否发生变化。

网络出口只是整个请求链路中的一个环节。即使出口地址固定,也不能排除 DNS、TCP、TLS、服务端处理和连接池配置导致的故障。

总结

API 偶尔失败、重试后恢复,并不意味着必须更换网络或者增加服务器资源。

更有效的做法,是先确定错误发生在哪个阶段,再结合 HTTP 客户端日志、curl 耗时、TCP 状态和服务端记录逐步定位。

如果问题集中在长时间运行的连接池,重点检查连接复用与空闲连接管理;如果 TCP 建连阶段耗时异常,则需要进一步检查路由、丢包和网络出口;如果连接已经建立但响应迟迟没有返回,则还需要分析服务端处理和上游依赖。

排障的核心不是让请求不断重试,而是让每次失败都能提供足够的信息,帮助我们缩小问题范围。

相关推荐
学Linux的语莫1 小时前
uv的使用
python·uv
asdzx671 小时前
使用 Python 按页或节拆分 Word 文档
python·word
用AI来创业吧1 小时前
企业官网搭建一般多久能上线:把工期拆成 7 段,算清哪段会拖
性能优化·自动化·安全威胁分析
技术人生0011 小时前
Selenium 工程化模板(日志+异常截图,可直接复制使用)
python·selenium·测试工具
我的xiaodoujiao1 小时前
Django 基础知识详细图文教程 15-Django 内置Admin系统
后端·python·django
夜晚回家1 小时前
Python 变量入门详解 | 用 BMI 案例串起赋值、乘方与取整
android·python
技术人生0011 小时前
Selenium4 Python 完整教程(适合网页自动化、页面数据采集、UI自动化测试)
python·ui·自动化
小小张说故事1 小时前
Python 里 a is b 有时 True 有时 False?小整数缓存和常量折叠的底层真相
python
IpdataCloud1 小时前
高精度IP定位服务怎么评估?从数据源、精度层级到实测验证的完整框架
服务器·网络·tcp/ip·ip