HTTP/2 多路复用怎么跑稳:从帧、流到流量控制

HTTP/2 多路复用示意

打开一个页面时,浏览器会同时请求 HTML、脚本、样式和图片。HTTP/1.1 通常需要多个连接来减少排队,HTTP/2 则把这些请求拆成可以交错发送的二进制帧,放进一条 TCP 连接。理解“帧、流、窗口”三个层次,才能解释为什么连接数变少了,某些慢请求却仍会拖住页面。

先分清帧、流和连接

HTTP/2 的最小传输单位是帧(Frame),常见类型包括 HEADERS、DATA、SETTINGS、WINDOW_UPDATE 和 RST_STREAM。每个帧都有一个 Stream Identifier,属于某条逻辑流;多个流共享同一个连接。响应头和响应体可以被拆成多个帧,与其他流的帧交错出现。

HTTP/2 帧交错传输

流 ID 是递增的奇数或偶数,分别由客户端和服务端发起。一个请求通常对应一条流,流结束后不必关闭 TCP 连接。于是,一个大图片在发送 DATA 帧时,旁边的小 CSS 请求仍有机会插入自己的帧,避免 HTTP/1.1 管线化那种应用层排队。

可以用 nghttp2 的调试客户端观察帧序列:

1
2
3
nghttp -nv https://example.com/
# 或检查 curl 是否协商到 HTTP/2
curl -sS -o /dev/null -w '%{http_version}\n' --http2 https://example.com/

输出中的 stream_id、帧类型和长度,比只看总耗时更能说明请求卡在头部、数据还是连接建立阶段。

多路复用没有消灭队头阻塞

HTTP/2 消除了 HTTP 层面的请求排队,却仍依赖有序可靠的 TCP 字节流。假设某个 TCP 包丢失,后续字节必须等重传包到达后才能交给 HTTP/2 解析器;此时所有流都会短暂等待,这就是传输层队头阻塞(HOL blocking)。因此,HTTP/2 的“一条连接”不是“永远不会互相影响”。

工程上应把它当成连接级故障域:连接建立失败、网络抖动或服务器发送窗口耗尽时,多个请求可能一起变慢。浏览器和客户端通常会在连接异常时重新建连,但服务端不应把“复用”理解成无限增大单连接的响应规模。大文件下载、长轮询和普通页面资源可以按业务拆分,并为长响应设置合理的超时与取消路径。

两级流量控制:发送前先看窗口

HTTP/2 使用接收方通告的窗口限制未确认的 DATA 字节数,避免快速发送方压垮接收方。窗口有两级:连接窗口约束所有流的总量,流窗口只约束单条流。接收方消费数据后发送 WINDOW_UPDATE,发送方才能继续发送。

连接窗口和流窗口

可以用下表快速定位现象:

现象 可能的窗口位置 排查方向
所有请求一起停顿 连接窗口耗尽 查看连接级 WINDOW_UPDATE、接收端读循环
只有一条下载变慢 流窗口耗尽 查看该 stream 的窗口与消费速度
CPU 不高但延迟持续上升 应用读取不及时 检查响应背压、线程池和下游写入

窗口调优不能只追求“越大越快”。窗口太小会增加 WINDOW_UPDATE 频率;窗口太大则会积压更多未消费数据,放大内存占用和取消成本。更稳妥的做法是先测量带宽、往返时延和响应大小,再用压测观察吞吐、P99 延迟及进程内存。

设置与排障清单

连接建立后,双方通过 SETTINGS 协商参数,例如初始流窗口和最大并发流数。修改初始窗口只影响后续窗口计算,已经建立的流仍要按协议处理。排查“HTTP/2 比 HTTP/1.1 慢”时,可以按这个顺序记录:

  1. TLS 握手后确认 ALPN 是否为 h2,排除其实回落到 HTTP/1.1。
  2. 记录每条流的首个 HEADERS、首个 DATA 和结束时间,区分服务端处理慢与网络等待。
  3. 对照连接级、流级窗口更新,确认是否存在长时间没有消费响应体的调用方。
  4. 在取消请求时发送 RST_STREAM,并确保应用层停止继续写入,避免无效数据占满窗口。

多路复用解决的是连接利用率和请求交错问题,流量控制解决的是“发送多少才安全”。把帧日志、窗口变化和应用读取速度放在同一条时间线上,才能找到真正的瓶颈,而不是盲目增加连接数或调大缓冲区。