在 Python 项目中调用 HTTP API 时,很多人会直接使用 requests.get() 完成请求。接口数量少、调用频率低时,这种写法通常没有问题。
但当程序需要连续调用多个接口,或者后台任务需要长时间运行时,可能会遇到一些现象:
-
相同的接口,第一次请求比较慢,后续请求速度有所变化。
-
偶尔出现连接超时,重试后又能恢复。
-
请求数量增加后,连接建立的开销变得明显。
-
程序运行一段时间后,开始出现连接异常。
这些问题不一定来自服务端,也可能与客户端的连接管理方式有关。本文通过 Python requests 和 HTTPAdapter,介绍如何复用 HTTP 连接,以及如何记录请求耗时、排查常见异常。
一、为什么需要 HTTP 连接池?
如果每次请求都单独创建连接,客户端就可能重复执行 DNS 解析、TCP 握手以及 TLS 握手等操作。
对于 HTTPS 请求来说,建立连接本身也需要消耗时间。频繁创建连接不仅增加额外开销,还会提高短时间内的连接数量。
HTTP 连接池的作用,是让客户端在合适的条件下复用已有连接,减少重复建立连接的成本。
在 Python 的 requests 库中,可以通过 Session 管理请求状态和连接复用。
最简单的示例如下:
import requests
session = requests.Session()
url = "https://httpbin.org/get"
for i in range(5):
response = session.get(url, timeout=(3, 10))
response.raise_for_status()
print(i, response.status_code)
这里的 Session 可以在多次请求之间复用连接。不过,能否复用还取决于目标地址、连接状态、响应是否被完整读取,以及服务端是否允许保持连接。
需要注意,连接池并不意味着所有请求都会使用同一条 TCP 连接。
二、使用 HTTPAdapter 配置连接池
当程序需要持续调用 API 时,可以使用 HTTPAdapter 设置连接池容量和重试策略。
例如:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
retry_strategy = Retry(
total=2,
connect=2,
read=0,
status=0,
backoff_factor=0.3,
allowed_methods=frozenset(["GET"]),
)
adapter = HTTPAdapter(
pool_connections=10,
pool_maxsize=20,
max_retries=retry_strategy,
pool_block=True,
)
session.mount("http://", adapter)
session.mount("https://", adapter)
response = session.get(
"https://httpbin.org/get",
timeout=(3, 10),
)
response.raise_for_status()
print(response.status_code)
几个关键参数需要注意:
-
pool_connections:连接池缓存的目标主机池数量。 -
pool_maxsize:每个连接池保留的连接容量。 -
pool_block=True:连接池达到容量上限时,等待可用连接,而不是继续创建超出池容量的连接。 -
max_retries:控制符合条件的连接失败等情况是否重试。
这里将重试限制为 GET 请求,并关闭读取错误与 HTTP 状态码的自动重试。
这样做是为了避免对所有异常进行无差别重试。对于支付、订单创建等可能产生副作用的操作,即使请求超时,也不能简单认定服务端没有执行成功。此时应结合业务幂等机制和请求记录判断是否可以重试。
另外,pool_block=True 时,等待连接池可用连接的时间不一定受 requests 的连接和读取超时参数完整约束。如果应用对排队时间有严格要求,还需要单独设计并发限制或连接池等待控制。
三、如何记录请求耗时?
只记录 HTTP 状态码,往往不足以判断请求为什么变慢。
可以先记录请求开始时间、结束时间、状态码以及异常类型,建立一份基础日志。
import time
import requests
def fetch(url):
started_at = time.perf_counter()
try:
response = requests.get(
url,
timeout=(3, 10),
)
elapsed = time.perf_counter() - started_at
print({
"url": url,
"status_code": response.status_code,
"elapsed_seconds": round(elapsed, 3),
})
response.raise_for_status()
return response.text
except requests.exceptions.Timeout:
print({
"url": url,
"error": "timeout",
"elapsed_seconds": round(
time.perf_counter() - started_at, 3
),
})
raise
except requests.exceptions.ConnectionError:
print({
"url": url,
"error": "connection_error",
"elapsed_seconds": round(
time.perf_counter() - started_at, 3
),
})
raise
fetch("https://httpbin.org/get")
这里使用 time.perf_counter() 测量程序执行耗时,适合进行持续时间统计。
不过,这段代码记录的是整个请求的总耗时,并不能直接区分 DNS、TCP、TLS 和服务端响应分别花了多长时间。如果需要进一步分析,可以使用 curl 进行分阶段测试:
curl -sS -o /dev/null \
--connect-timeout 3 \
--max-time 15 \
-w 'dns=%{time_namelookup}s tcp=%{time_connect}s tls=%{time_appconnect}s ttfb=%{time_starttransfer}s total=%{time_total}s\n' \
https://httpbin.org/get
输出中的几个指标分别表示:
-
time_namelookup:DNS 解析耗时。 -
time_connect:建立 TCP 连接所需时间。 -
time_appconnect:HTTPS 场景下完成 TLS 握手所需时间。 -
time_starttransfer:从请求开始到收到首字节的时间。 -
time_total:整个请求的总耗时。
这些指标有助于缩小排查范围,但具体耗时还会受到网络路径、连接复用、DNS 缓存和目标服务状态等因素影响。
四、遇到超时和连接异常,应该怎么排查?
实际排查时,可以先根据异常类型进行分类,而不是统一执行重试。
| 异常表现 | 优先检查的方向 |
|---|---|
ConnectTimeout |
目标地址、路由、防火墙、服务端监听状态 |
ReadTimeout |
服务端处理时间、接口负载、响应是否迟迟未返回 |
ConnectionError |
DNS、TCP 连接、连接被重置、网络中断 |
| HTTP 429 | 请求频率限制和服务端限流策略 |
| HTTP 5xx | 服务端错误、网关状态、上游依赖 |
| HTTP 4xx | 请求参数、身份认证、权限和业务规则 |
例如,接口偶尔出现 ReadTimeout,不一定说明网络连接建立失败。客户端可能已经成功建立连接并发送请求,只是没有在规定时间内收到完整响应。
此时应该结合服务端日志、请求 ID、接口耗时和错误发生频率进行判断。
如果错误集中发生在某个时间段,还可以对比当时的并发数量、连接池使用情况和服务端负载。
对于长时间运行的程序,建议至少记录以下字段:
-
请求时间与目标主机。
-
请求耗时与 HTTP 状态码。
-
异常类型及重试次数。
-
业务请求 ID。
-
必要的网络诊断信息。
不要在日志中直接记录密码、访问令牌、Cookie 或其他敏感数据。
五、连接池使用中的几个常见误区
误区一:连接池越大越好。
连接池容量应与并发量、目标服务的承载能力以及客户端资源相匹配。连接池设置过大,可能增加并发连接数量和资源消耗,并不一定提高吞吐量。
误区二:设置重试就能解决网络问题。
重试只能处理部分暂时性故障,无法解决持续性的 DNS 错误、网络中断、服务端限流或参数错误。对于非幂等请求,重试还可能导致重复执行。
误区三:请求成功就说明连接复用正常。
单次请求成功只能证明该次请求完成,不能证明连接池配置合理。要评估连接复用效果,应在相同条件下进行多轮测试,并观察耗时分布、连接数量和错误率。
误区四:客户端超时就是服务端故障。
超时可能发生在连接建立阶段,也可能发生在等待响应阶段。应先明确异常类型,再结合客户端与服务端日志判断原因。
六、总结
Python HTTP 连接池的价值,在于减少重复建立连接的开销,并帮助程序更好地管理持续请求。
在实际项目中,可以从三个方面入手:
-
使用
requests.Session复用连接。 -
根据并发需求配置
HTTPAdapter,并限制重试范围。 -
记录请求耗时和异常类型,再结合
curl等工具分析连接链路。
对于定时任务、后台服务和 AI Agent 等需要持续调用外部 API 的程序,这些基础措施能够让网络问题更容易复现、定位和分析。
与其在请求失败后不断增加重试次数,不如先弄清楚请求在哪个阶段失败,再针对具体原因调整连接池、超时和并发策略。