深入理解 HTTP/2 多路复用

· #HTTP/2 #网络 #后端

HTTP/1.1 的队头阻塞(Head-of-Line Blocking)是 Web 性能的一个老问题。HTTP/2 通过多路复用解决了它,但引入了新的复杂度。这篇文章梳理一下 HTTP/2 多路复用的工作原理和实际应用中的注意事项。

HTTP/1.1 的问题

在 HTTP/1.1 中,每个 TCP 连接同一时间只能处理一个请求-响应。浏览器通过打开多个连接(通常 6 个)来并行加载资源,但这带来了连接管理的开销。更致命的是队头阻塞:即使后面的请求已经就绪,也必须等前面的慢请求完成才能使用这个连接。

常见的变通方案——CSS sprites、资源内联、域名分片——本质都是在绕过协议的限制而非解决问题。

多路复用的核心:帧和流

HTTP/2 引入了一个二进制分帧层。每个 HTTP 消息被拆成多个帧(HEADERS 帧、DATA 帧),在同一个 TCP 连接上以流(Stream)的形式交错传输。接收端根据帧头中的 Stream ID 重新组装。

TCP 连接
  ├── Stream 1: [HEADERS] [DATA] [DATA]       ← index.html
  ├── Stream 3: [HEADERS] [DATA]              ← style.css
  ├── Stream 5: [HEADERS] [DATA] [DATA] ...   ← bundle.js
  └── Stream 7: [HEADERS] [DATA]              ← logo.png

关键设计:

HPACK 头部压缩

多路复用让头部压缩变得更有价值。HTTP/1.1 中每个请求都要发送完整的 Cookie 和 User-Agent(通常几百字节)。在 HTTP/2 中,同一个连接上的请求共享一个 HPACK 动态表,后续请求只需要发送变化的部分。

实际测试中,对于典型的 API 调用(Authorization header + 少量自定义 header),HPACK 能把第二个请求起的头部从 400 字节压缩到几十字节。

新的队头阻塞

HTTP/2 解决了应用层的队头阻塞,但在传输层仍然受影响——TCP 的可靠传输要求数据按序到达。如果 TCP 层丢了一个包,后续所有流的数据都会被阻塞,即使它们在应用层没有依赖关系。

这就是 HTTP/3 选择 QUIC(基于 UDP)而非 TCP 的原因。QUIC 在传输层也实现了流的多路复用,丢包只影响单个流。

Nginx 配置要点

# 启用 HTTP/2(需要 TLS)
listen 443 ssl http2;

# 调整流并发数(默认 128)
http2_max_concurrent_streams 256;

# 控制 HPACK 表大小
http2_max_header_size 16k;
http2_recv_buffer_size 256k;

几个实践建议:

  1. 域名分片反而有害:多路复用让单个连接就够了,多域名反而增加 DNS 查询和 TLS 握手开销
  2. 服务器推送要谨慎:推送的资源如果客户端已有缓存就是纯浪费。用 Link rel=preload 配合 nopush 属性更灵活
  3. 小文件合并不再必要:CSS sprites 和文件拼接带来的收益在 HTTP/2 下大打折扣,反而增加了缓存失效的粒度

监控指标

如果使用 Nginx,以下指标值得关注:

HTTP/2 的多路复用看似简单——不过是把一个连接上的请求交错发送——但背后的帧协议、流量控制、优先级调度和头部压缩构成了一套精心设计的系统。理解这些细节,才能在遇到奇怪的性能问题时知道该往哪里看。