bigywater.tech
爬虫工程 · 11

HTTP/2 多路复用:抓取并发的实际收益

“上 HTTP/2 就更快”是爬虫圈里常见的说法。这篇在本机用 nginx 起一个真实的 HTTP/2 服务,把客户端库、并发数都对齐之后,看 HTTP/2 到底换来了什么:连接数、耗时、服务器的流上限,以及队头阻塞。

1 对 50
并发 50 个请求时,HTTP/2 与 HTTP/1.1 用到的连接数
111 对 121 ms
同样并发 50、服务器每个请求停 0.1 秒:HTTP/2 和 HTTP/1.1 的总耗时,几乎一样
28 对 985 ms
慢请求在前时,20 个快请求最晚完成的时间:HTTP/2 一条连接对 HTTP/1.1 一条连接

协商出 HTTP/2

实测 · 本机执行输出
  声明 h2 和 http/1.1 → 协商结果 h2,TLS 版本 TLSv1.3
  只声明 http/1.1 → 协商结果 http/1.1,TLS 版本 TLSv1.3
  nginx 访问日志里的协议列:/r → HTTP/1.1, /c → HTTP/2.0(requests 只会说 HTTP/1.1,curl_cffi 指定了 v2)

HTTP/2 通过 TLS 握手里的 ALPN 协商。客户端声明 h2 且服务器支持就用 HTTP/2,只声明 http/1.1 就是 HTTP/1.1。requests 只会说 HTTP/1.1,所以这篇改用 curl_cffi。

总耗时由并发数决定

实测 · 本机执行输出
  协议          并发数     连接数      总耗时中位数
  HTTP/1.1      1       1      5186 ms(只跑了 1 次)
  HTTP/1.1      6       6       948 ms
  HTTP/1.1     50      50       121 ms
  HTTP/2        1       1      5186 ms(只跑了 1 次)
  HTTP/2       10       1       526 ms
  HTTP/2       50       1       111 ms

服务器每个请求停 0.1 秒时,50 个请求的总耗时取决于同时飞行多少个:并发 1 约 5.2 秒、并发 6 约 0.95 秒、并发 50 约 0.12 秒。HTTP/2 在并发 50 时只用一条连接,耗时 111 ms,与 HTTP/1.1 开 50 条连接的 121 ms 差不多。注意 curl_cffi 的默认并发上限(max_clients)是 10,不改的话 50 个请求要分 5 批,和 HTTP/2 本身无关。

连接开销

实测 · 本机执行输出
  HTTP/1.1 并发 50:122 条连接,每条连接上的请求数(前 3 大)=[11, 11, 11],总耗时中位数 131 ms
  HTTP/2   并发 50: 1 条连接,每条连接上的请求数(前 3 大)=[600],总耗时中位数 94 ms
  HTTP/1.1 并发  6:41 条连接,每条连接上的请求数(前 3 大)=[51, 39, 35],总耗时中位数 146 ms
  HTTP/2   并发  6: 1 条连接,每条连接上的请求数(前 3 大)=[600],总耗时中位数 119 ms

服务器不延迟时,请求本身几乎不花时间,建立连接和握手的开销就明显了:600 个请求并发 50,HTTP/1.1 开了上百条连接、131 ms,HTTP/2 一条连接、94 ms。这是本机回环的结果,没有真实网络的往返时间;真实网络上每条新连接还要多付往返,差距只会更大(我没有测)。

服务器的流上限

实测 · 本机执行输出
  max_concurrent_streams=128:客户端用了 1 条连接(每条上的请求数前 3 大 [50]),总耗时中位数 111 ms
  max_concurrent_streams=10 :客户端用了 5 条连接(每条上的请求数前 3 大 [10, 10, 10]),总耗时中位数 114 ms
  max_concurrent_streams=2  :客户端用了 25 条连接(每条上的请求数前 3 大 [2, 2, 2]),总耗时中位数 125 ms

服务器声明的最大并发流数限制了一条连接能同时承载多少请求。上限 128 时 50 个请求用 1 条连接;上限 10 时客户端自动开了 5 条;上限 2 时开了 25 条。这说明“HTTP/2 一条连接搞定一切”取决于服务器配置。流上限设得很小时,我遇到过一次 curl_cffi 报 Connection died,没有查原因。

队头阻塞

实测 · 本机执行输出
  HTTP/1.1 一条连接(串行):慢请求完成于 507 ms;20 个快请求最早完成于 532 ms、最晚完成于 985 ms;全部 21 个完成于 985 ms
  HTTP/1.1 六条连接:慢请求完成于 508 ms;20 个快请求最早完成于 23 ms、最晚完成于 101 ms;全部 21 个完成于 508 ms
  HTTP/2 一条连接:慢请求完成于 507 ms;20 个快请求最早完成于 26 ms、最晚完成于 28 ms;全部 21 个完成于 507 ms

HTTP/1.1 一条连接上请求按顺序处理,慢请求排在前面,后面的快请求都要等(最晚 985 ms 才完成)。HTTP/2 在同一条连接上并行处理,快请求在 28 ms 内全部完成。开多条 HTTP/1.1 连接也能缓解(101 ms),代价是更多的连接。这里的慢请求只是服务器端的延迟;TCP 层的队头阻塞(丢包时影响整条连接)本机回环里测不出来。

分步演示

HTTP/2 多路复用
本步的对象

    本文用到的工具与边界

    • 本机 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/2 服务端是本机 nginx 1.31.6(只监听 127.0.0.1,自签名证书,独立 prefix),反向代理到我的 Python 后端(后端按查询串 d 停顿);客户端是 curl_cffi 0.15.0(libcurl,开启 PIPEWAIT 让并发请求等待复用同一条连接),HTTP/1.1 与 HTTP/2 都用它,max_clients 对齐并发数;连接数与协议取自 nginx 访问日志的 $connection 和 $server_protocol;每个配置跑 3 次(无延迟的 600 请求跑 5 次)取总耗时中位数,并发 1 的只跑了 1 次
    • 没有验证:真实网络延迟与丢包下的表现(TCP 层的队头阻塞);HTTP/3;其它支持 HTTP/2 的客户端(httpx 等没有安装);不开 PIPEWAIT 时的连接数(默认会因为并发建连而开出多条连接);流上限为 1 或 2 时偶发的 Connection died 的原因;aiohttp(只支持 HTTP/1.1)的对比;服务端是 nginx 以外的实现时的行为
    京ICP备2026067502号-1京公网安备11010502063487号