bigywater.tech
微服务 · 35

HTTP/2 到网关、HTTP/1.1 到上游:并发与队头阻塞

前端用 HTTP/2、后端仍是 HTTP/1.1,并发和队头阻塞各自落在哪一段?流数上限怎么影响总耗时?TCP 丢包时单条连接的 HTTP/2 比多条连接的 HTTP/1.1 差在哪?这篇用 nginx 1.31.6、curl、h2 库和 dummynet 丢包量出来。

60 条
client 用 HTTP/2 一条连接并行 60 个请求时,nginx 向上游(HTTP/1.1)建立的连接数;耗时 0.14 s,HTTP/1.1 串行是 6.46 s
1.10 → 0.18 s
http2_max_concurrent_streams 从 32 提到 512 时,300 个请求的总耗时
4.0 倍 对 2.1 倍
3% 丢包下,HTTP/2 一条连接与 HTTP/1.1 6 条连接的总耗时相对无丢包时变慢的倍数

前后端的并发

实测 · 本机执行输出
  client 方式                | 60 个请求总耗时 | 状态码         | 上游同时在途的请求峰值 | nginx 向上游建立的连接数
  -------------------------+-----------+-------------+-------------+----------------
  (a) HTTP/1.1,1 条连接串行     | 6.46 s    | {'200': 60} | 1           | 1              
  (b) HTTP/1.1,6 条并行连接     | 1.10 s    | {'200': 60} | 6           | 6              
  (c) HTTP/2,1 条连接 60 个并行流 | 0.14 s    | {'200': 60} | 60          | 60             

HTTP/2 在前端复用一条连接,但上游是 HTTP/1.1 时每个并行的流占一条上游连接。

流数上限

实测 · 本机执行输出
  http2_max_concurrent_streams | client 看到的上限 | 300 个请求总耗时 | 状态码          | 上游同时在途峰值 | nginx 向上游建立的连接数
  -----------------------------+--------------+------------+--------------+----------+----------------
  32                           | 32           | 1.10 s     | {'200': 300} | 32       | 32             
  128                          | 128          | 0.36 s     | {'200': 300} | 128      | 162            
  512                          | 512          | 0.18 s     | {'200': 300} | 296      | 300            
  HTTP/2 的并发流数受服务端公布的上限约束:超过的流要等已有的流结束才能开始,所以总耗时大约是 ⌈300 ÷ 上限⌉ 个 0.1 s;上游是 HTTP/1.1,每个并行的流占一条上游连接,上游的峰值并发等于同时在途的流数(受上限约束)

超过上限的流要排队,总耗时大约是 ⌈请求数 ÷ 上限⌉ 个单次耗时。

丢包与队头阻塞

实测 · 本机执行输出
  丢包率 | client 方式        | 100 个请求总耗时(中位数) | 3 轮范围         | 请求完成时间 p50 | p99   
  ----+------------------+-----------------+---------------+------------+-------
  0%  | HTTP/1.1,1 条连接串行 | 1.44 s          | 1.32 ~ 1.47 s | 0.01 s     | 0.02 s
  0%  | HTTP/1.1,6 条并行连接 | 0.28 s          | 0.28 ~ 0.28 s | 0.02 s     | 0.02 s
  0%  | HTTP/2,1 条连接     | 0.08 s          | 0.08 ~ 0.10 s | 0.05 s     | 0.05 s
  3%  | HTTP/1.1,1 条连接串行 | 2.44 s          | 2.42 ~ 3.09 s | 0.02 s     | 0.13 s
  3%  | HTTP/1.1,6 条并行连接 | 0.59 s          | 0.52 ~ 0.60 s | 0.02 s     | 0.15 s
  3%  | HTTP/2,1 条连接     | 0.32 s          | 0.32 ~ 0.46 s | 0.06 s     | 0.30 s
  丢包后总耗时相对无丢包时的倍数:HTTP/1.1,1 条连接串行 1.7 倍;HTTP/1.1,6 条并行连接 2.1 倍;HTTP/2,1 条连接 4.0 倍。HTTP/2 的所有流共用一条 TCP 连接,一个包丢了,这条连接上所有流的后续数据都要等重传(TCP 层的队头阻塞);HTTP/1.1 的多条连接各自独立。看倍数而不是绝对值:这个实验里 HTTP/2 在有丢包时的绝对耗时仍然最短,但它相对无丢包时变慢得更多

一条 TCP 连接上的所有流共享重传;绝对耗时仍短,但相对退化更多。

分步演示

HTTP/2 到网关 HTTP/1.1 到上游
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。nginx 是 Homebrew 的 1.31.6(listen 加 http2 on,明文 h2c);curl 8.7.1(--http2-prior-knowledge,--parallel);第二节是 h2 4.3.0 写的最小 client(装在 ttmp_venv);丢包用 dummynet 辅助脚本 netlab(只作用于 lo0 端口 9400–9499,3% 丢包,每个方向 2 ms 延迟);上游是原始 TCP HTTP/1.1 服务;第三节每种配置 3 轮
    • 没有验证:HTTP/2 over TLS(浏览器的实际情形)与 ALPN;HTTP/3 / QUIC 如何避免 TCP 层队头阻塞(见网络系列);更高丢包率与更大延迟;http2_max_requests 与 keepalive 的交互;HPACK 头部压缩的效果;curl 的内部排队对 time_total 的影响
    京ICP备2026067502号-1京公网安备11010502063487号