
打开一个页面时,浏览器会同时请求 HTML、脚本、样式和图片。HTTP/1.1 通常需要多个连接来减少排队,HTTP/2 则把这些请求拆成可以交错发送的二进制帧,放进一条 TCP 连接。理解“帧、流、窗口”三个层次,才能解释为什么连接数变少了,某些慢请求却仍会拖住页面。
先分清帧、流和连接
HTTP/2 的最小传输单位是帧(Frame),常见类型包括 HEADERS、DATA、SETTINGS、WINDOW_UPDATE 和 RST_STREAM。每个帧都有一个 Stream Identifier,属于某条逻辑流;多个流共享同一个连接。响应头和响应体可以被拆成多个帧,与其他流的帧交错出现。

流 ID 是递增的奇数或偶数,分别由客户端和服务端发起。一个请求通常对应一条流,流结束后不必关闭 TCP 连接。于是,一个大图片在发送 DATA 帧时,旁边的小 CSS 请求仍有机会插入自己的帧,避免 HTTP/1.1 管线化那种应用层排队。
可以用 nghttp2 的调试客户端观察帧序列:
1 | nghttp -nv 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 慢”时,可以按这个顺序记录:
- TLS 握手后确认 ALPN 是否为
h2,排除其实回落到 HTTP/1.1。 - 记录每条流的首个 HEADERS、首个 DATA 和结束时间,区分服务端处理慢与网络等待。
- 对照连接级、流级窗口更新,确认是否存在长时间没有消费响应体的调用方。
- 在取消请求时发送 RST_STREAM,并确保应用层停止继续写入,避免无效数据占满窗口。
多路复用解决的是连接利用率和请求交错问题,流量控制解决的是“发送多少才安全”。把帧日志、窗口变化和应用读取速度放在同一条时间线上,才能找到真正的瓶颈,而不是盲目增加连接数或调大缓冲区。