bigywater.tech
爬虫工程 · 2

连接复用:Session 与每次新建连接,一次请求差多少毫秒

爬虫里最常见的优化建议是“用 Session”。这篇不停留在结论上:在本机起一个会记录连接的测试服务器,看 requests.get 和 Session 各自到底开了多少条连接、省下多少时间,再看看哪些写法会让 Session 白用。

300 对 1
300 次请求:每次 requests.get 用 300 条连接,一个 Session 用 1 条
4.34 倍
HTTPS 下每次新建连接与 Session 的单次耗时比(HTTP 下是 1.55 倍)
4 条
stream=True 不读也不关时,4 次请求各用了一条连接

HTTP:每次新建与 Session

实测 · 本机执行输出
  每次 requests.get:每轮平均 0.48, 0.47, 0.54, 0.54, 0.50 ms;中位数 0.50 ms
  同一个 Session  :每轮平均 0.31, 0.30, 0.34, 0.33, 0.35 ms;中位数 0.33 ms
  服务器共接受了 1505 条连接,收到 3000 个请求(5 轮 × 2 种做法 × 300 = 3000 个请求)
  两种做法的耗时比(每次新建 / Session)= 1.55
  单独统计:300 次 requests.get → 服务器接受 300 条连接,每条连接上的请求数 [1]
  单独统计:一个 Session 发 300 次 → 服务器接受 1 条连接,每条连接上的请求数 [300]

requests.get 每次调用在内部临时建一个 Session,用完就关(源码 requests/api.py 里就是 with sessions.Session() as session),所以每次调用都是一条新连接。服务器记录的结果和这一点吻合:300 次调用对应 300 条连接。同一个 Session 复用同一条连接发了 300 个请求。

回环网络的往返时间接近 0,所以这里 0.50 ms 对 0.33 ms 的差距基本只是建立连接本身的开销;真实网络上每条新连接还要多付至少一个往返,本文没有测。

HTTPS:握手的代价

实测 · 本机执行输出
  每次 requests.get:每轮平均 1.51, 1.51, 1.56, 1.51, 1.57 ms;中位数 1.51 ms
  同一个 Session  :每轮平均 0.37, 0.35, 0.34, 0.33, 0.35 ms;中位数 0.35 ms
  服务器共接受了 755 条连接,收到 1500 个请求(5 × 2 × 150 = 1500)
  两种做法的耗时比(每次新建 / Session)= 4.34

HTTPS 下每次新建连接要多做 TLS 握手,单次耗时的比值从 1.55 倍拉到 4.34 倍。绝对值很小(本机回环、EC 证书),但它是每个请求都要付的固定成本。差额主要来自握手只是我的推断——没有把握手单独计时,也没有对比不同的密钥类型。

Session 白用的情形 (1):响应没读完

实测 · 本机执行输出
  stream=True,不读也不关 → 服务器接受 4 条连接;各连接上的请求数 [1, 1, 1, 1]
  stream=True,读完 r.content → 服务器接受 1 条连接;各连接上的请求数 [4]
  stream=True,不读直接 r.close() → 服务器接受 4 条连接;各连接上的请求数 [1, 1, 1, 1]

复用连接要求上一个响应已经被读完。stream=True 的响应如果既不读也不关,连接还被它占着,下一个请求只能新开;读完 r.content 才会把连接还回池里。不读直接 r.close() 也不行:Response.close() 在内容还没消费时先调用 raw.close(),底层连接被关掉,下次要重新建立。只想看响应头就丢掉的写法,会让 Session 的收益消失。

Session 白用的情形 (2):服务器不保持连接

实测 · 本机执行输出
  服务器每次回 Connection: close 并关闭:Session 发 5 次 → 服务器接受 5 条连接,各连接上的请求数 [1, 1, 1, 1, 1]
  客户端发 Connection: close,服务器照办(回 Connection: close 并关闭):Session 发 5 次 → 5 条连接,各连接上的请求数 [1, 1, 1, 1, 1]
  客户端发 Connection: close,但服务器无视并保持连接:Session 发 5 次 → 1 条连接,各连接上的请求数 [5]

服务器回 Connection: close 并关闭连接,Session 每次也要新建。客户端自己在请求里写 Connection: close 只是“请求服务器关闭”:服务器照办时同样是 5 次 5 条连接;服务器无视它时,Session 仍然复用同一条连接。urllib3 从池里取连接时会检查连接是否已经断开(is_connection_dropped),断开了就新建。

分步演示

Session 连接复用
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。HTTP 与 HTTPS 各对比每次 requests.get 和同一个 Session,交替跑 5 轮,取每轮平均后的中位数;响应体 200 字节;HTTPS 用 openssl 生成的 EC P-256 自签名证书并让 requests 信任它;服务器是 Python 标准库的 http.server(HTTP/1.1),记录每条连接上的请求数
    • 没有验证:真实网络(有往返时间、有丢包)下的收益;把 TLS 握手单独计时;连接池大小不够时的行为(下一篇);HTTP/2;其它客户端库(aiohttp、httpx 等)的复用规则;服务器 keep-alive 超时到期后客户端的表现(见后面的文章)
    京ICP备2026067502号-1京公网安备11010502063487号