Python HTTP 连接池怎么用?从连接复用到请求异常排查

在 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 连接池的价值,在于减少重复建立连接的开销,并帮助程序更好地管理持续请求。

在实际项目中,可以从三个方面入手:

  1. 使用 requests.Session 复用连接。

  2. 根据并发需求配置 HTTPAdapter,并限制重试范围。

  3. 记录请求耗时和异常类型,再结合 curl 等工具分析连接链路。

对于定时任务、后台服务和 AI Agent 等需要持续调用外部 API 的程序,这些基础措施能够让网络问题更容易复现、定位和分析。

与其在请求失败后不断增加重试次数,不如先弄清楚请求在哪个阶段失败,再针对具体原因调整连接池、超时和并发策略。

相关推荐
摇滚侠1 小时前
《Java并发编程之美》第 4 章 Java 并发包中原子操作类原理剖析 阅读笔记 7
java·笔记·python
用户8356290780511 小时前
如何使用 Python 在 Word 文档中创建和格式化列表
后端·python
码林鼠1 小时前
flutter状态管理入门教程-Riverpod
开发语言·javascript·flutter
system_error_x1 小时前
C++ 编程基础入门教程
开发语言·c++
羡林i1 小时前
ProcessPoolExecutor 传参踩坑实测:pickle 失败的四种写法
python
Amos_Web2 小时前
Markdonw语法
开发语言·前端·javascript
Dmz丶2 小时前
前端转 AI 100 天 Day 30:正式上线——从零部署一个完整服务(含 30 天复盘)
python
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 回顾和今日内容介绍
java·开发语言·交友
稻草人4782 小时前
安装OCA模块
python