上一篇讲 keep-alive 时埋了个钩子:它解决了重复握手,但没解决请求串行。这个「请求串行」有个专门的名字——队头阻塞(Head-of-Line Blocking,HOL)。它是 HTTP/1.1 最深的坑,也是推动 HTTP/2、HTTP/3 两代协议演进的直接动力。

这一篇把队头阻塞讲透。它有两个层面,理解了这两层,你才会明白为什么 HTTP/2 多路复用只解决了一半,而 HTTP/3 必须换掉 TCP。

应用层队头阻塞:请求串行,前慢后等

keep-alive 让一条 TCP 连接可以跑多个请求,但 HTTP/1.1 的规则是:一条连接上,请求必须按顺序一个个来。客户端发了请求 1,必须等请求 1 的响应回来,才能发请求 2。

应用层队头阻塞:串行请求,慢响应阻塞后续

问题出在「响应顺序」上。假设一条连接上排了三个请求:请求 1(查列表)、请求 2(查详情)、请求 3(查评论)。请求 1 的响应特别慢(比如服务器在慢查询),这时候请求 2、请求 3 的响应哪怕早就生成好了,也得排在请求 1 后面干等——因为 HTTP/1.1 要求响应必须按请求顺序返回。

这就是队头阻塞的字面意思:队列头部(请求 1)阻塞了,后面整队都得等

浏览器的应对是开多条连接(上一篇讲的 6 连接上限),让 6 个请求能并行跑。但连接数有上限,而且每条连接内部还是串行的。域名分片是把 6 这个数字凑大的 workaround,但没从根本上解决问题。

HTTP/1.1 还有个叫「管线化(pipelining)」的特性——客户端可以连续发多个请求不用等响应。但它要求服务器按顺序返回响应,所以慢响应照样阻塞快响应,没解决根本问题,加上中间件兼容性差,实际几乎没人用,HTTP/2 直接废弃了它。

TCP 层队头阻塞:丢一个包,整窗停摆

应用层队头阻塞之外,还有更底层的 TCP 层队头阻塞,这个更隐蔽。

TCP 层队头阻塞:丢包阻塞整个连接窗口

TCP 的核心承诺是「有序、可靠」。它给数据编号,接收方必须按顺序交给上层。如果传输中丢了一个包(比如包 2 丢了),接收方收到了包 1、3、4,它会把 3、4 缓存住,但不能交给上层——因为缺了 2,交给上层就乱序了。发送方发现包 2 没被确认,重传包 2,等包 2 到了,才能把 2、3、4 一起按顺序交付。

这个机制保证了可靠性,但代价是:一个包丢了,后面已经到达的包全部卡住。这就是 TCP 层的队头阻塞。

在 HTTP/1.1 时代,一条连接一次只跑一个请求,TCP 层 HOL 影响的是「一个请求」。但 HTTP/2 多路复用后,一条连接上同时跑多个请求(多个 stream),TCP 层一个包丢了,这条连接上所有 stream 都得等——哪怕丢的只是 stream A 的包,stream B 的数据也得跟着卡。

这是个很讽刺的局面:HTTP/2 用多路复用解决了应用层 HOL,但因为还在 TCP 上跑,反而把 TCP 层 HOL 的影响放大了。这正是 HTTP/3 要彻底换掉 TCP、改用基于 UDP 的 QUIC 的根本原因。

域名分片:HTTP/1.1 时代的 workaround

回到应用层 HOL。HTTP/1.1 时代的「解法」是域名分片:

域名分片 workaround vs HTTP/2 多路复用正解

把资源分散到 static1.xxx.comstatic2.xxx.com...每个域名都能开 6 条连接,凑出更多并行度。比如分到 4 个域名,就有 4×6=24 条连接,24 个请求能并行。

但域名分片有三个代价:

  • 更多握手开销。每个新域名都要 DNS 解析 + TCP 握手 + TLS 握手,反而抵消了一部分 keep-alive 的收益。
  • DNS 解析变多。每个域名都要 DNS 查询,增加延迟。
  • 破坏缓存。如果资源本来能在同域名被 HTTP/2 多路复用优化,硬拆成多域名反而让 HTTP/2 优化失效。

所以域名分片是典型的「头痛医头」——它绕过了应用层 HOL,但引入了新开销。HTTP/2 多路复用出来后,域名分片基本被淘汰了:HTTP/2 一条连接就能跑无数个并发请求,不需要拆域名。

取舍与边界

理解队头阻塞,关键看它发生在哪一层:

  • 应用层 HOL(HTTP/1.1 的请求串行)→ 由 HTTP/2 多路复用解决。
  • TCP 层 HOL(丢包阻塞整个连接)→ HTTP/2 没解决,反而放大了,由 HTTP/3 的 QUIC 解决。

这两层 HOL 的递进,构成了 HTTP 协议两代演进的核心动机。HTTP/2 填了应用层的洞,HTTP/3 填了传输层的洞。后面的篇章会分别讲它们怎么填的。

收束:HOL 是连接层的结构性问题

队头阻塞不是 bug,它是 HTTP/1.1 和 TCP 设计取舍的必然结果。HTTP/1.1 为了简单让请求串行,TCP 为了可靠让数据有序——这两个「为了」都合理,但叠加起来就成了瓶颈。

keep-alive 让连接复用成为可能,但没动这个瓶颈。下一篇暂时离开「请求并发」的话题,讲 HTTPS 的加密层——TLS 握手。它和 TCP 握手一样,是连接建立的另一笔开销,理解它才能看懂 HTTPS 为什么比 HTTP 慢、以及怎么优化。


关于十三Tech

我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。

我相信 AI 是程序员的最佳搭档,也希望帮助每一位开发者更好地驾驭 AI。

如果你想继续跟完这套「图解 HTTP」,欢迎关注公众号 「十三Tech」。后续会按 URL 与报文、连接与传输、缓存与协商、安全与边界、HTTP/2 与 HTTP/3 这条线更新。

十三Tech公众号二维码