“上 HTTP/2 就更快”是爬虫圈里常见的说法。这篇在本机用 nginx 起一个真实的 HTTP/2 服务,把客户端库、并发数都对齐之后,看 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 层的队头阻塞(丢包时影响整条连接)本机回环里测不出来。