什么是队头阻塞(HOL Blocking)?HTTP/1.1、HTTP/2、HTTP/3 分别怎么解决?
简化版
队头阻塞(Head-of-Line Blocking)指排在队伍最前面的那个请求/数据卡住了,后面的全都得跟着等,哪怕后面的早就准备好了。HTTP 里它出现在两个层面:HTTP 层——HTTP/1.1 一条连接同一时刻只能处理一个请求,前一个响应没回来后面的就排队;TCP 层——TCP 要求字节严格按序交付,中间某个包丢了,后面的包即使到了也得在内核缓冲区里等重传。HTTP/2 用多路复用解决了 HTTP 层的队头阻塞(一条连接并发多个流),但因为仍跑在 TCP 上,没解决 TCP 层的队头阻塞;HTTP/3 换用基于 UDP 的 QUIC,让每个流独立管理丢包重传,才把 TCP 层的队头阻塞也解决了。
详细版
队头阻塞的两个层面:
| 层面 | 谁受影响 | 出现在 |
|---|---|---|
| HTTP 层队头阻塞 | 应用请求排队 | HTTP/1.1 |
| TCP 层队头阻塞 | 一个流的丢包拖累所有流 | HTTP/1.1、HTTP/2(都跑在 TCP 上) |
① HTTP/1.1 的队头阻塞
HTTP/1.1 一条 TCP 连接上,请求必须一问一答:发出请求 A,要等 A 的响应完整回来,才能发请求 B。如果 A 的响应很慢(比如后端在算一个大报表),B、C、D 全都干等着。虽然浏览器靠开 6 个并发连接缓解,但连接数有限,本质没解决。(HTTP/1.1 曾有个 Pipelining 想让请求批量发出,但服务端仍要按顺序返回响应,队头阻塞照旧,且兼容性差,基本没人用。)
② HTTP/2 的解决:多路复用
HTTP/2 引入流(Stream) 和帧(Frame):一条 TCP 连接上可以有很多个流并发,每个请求/响应拆成帧,帧上带 Stream ID,交错着传,到对端再按 ID 重新组装。这样请求 A 慢,请求 B 的帧照样能穿插着发回来——HTTP 层的队头阻塞解决了。
③ HTTP/2 仍存在 TCP 层队头阻塞
HTTP/2 的所有流共用一条 TCP 连接。TCP 保证字节严格按序交付:如果流 A 的一个 TCP 包丢了,TCP 会卡住整条连接的数据交付(后面的包哪怕属于流 B、已经到了,也得在内核里排队等 A 的包重传补齐)。于是「一个流丢包,全部流卡住」——TCP 层的队头阻塞。在丢包率高的网络上,HTTP/2 有时反而不如多连接的 HTTP/1.1。
④ HTTP/3 的解决:QUIC over UDP
HTTP/3 抛弃 TCP,改用 QUIC(跑在 UDP 上,自己实现可靠传输)。QUIC 在协议层就理解「流」的概念,每个流独立编号、独立管理丢包和重传:流 A 丢包只需重传流 A 的数据,流 B 的数据可以照常交付,互不干扰。这样TCP 层的队头阻塞也被消除。
完整版教学
一、先建立直觉:什么是「队头阻塞」
想象超市只开了一个收银台,排在最前面的顾客要退货、要理论半天,后面所有人不管东西多少、多急,都得等。这就是队头阻塞:串行队列里,队头的慢会拖累整条队伍。
网络里凡是「必须按顺序处理」的地方,都可能出现队头阻塞。HTTP 的性能演进史,很大程度上就是一步步消灭队头阻塞的历史。要讲清楚,必须分清它藏在两个不同的层。
二、HTTP/1.1:应用层的队头阻塞
HTTP/1.1 的连接模型是请求-响应串行。一条 TCP 连接上,客户端发出请求后,必须等这个请求的响应完整回来,才能在这条连接上发下一个请求。
连接上的时间线(HTTP/1.1):
请求A ───────────►
◄─────── 响应A(很慢)
请求B ───────────► ← B 一直等到 A 回完才能发
◄─────── 响应B
为什么浏览器还挺快? 因为浏览器对同一域名开多条并发连接(主流浏览器默认 6 条)。6 条连接 = 6 个收银台,能同时处理 6 个请求。但这带来新问题:
- 连接数有限(6 条),页面上几十上百个资源仍要排队。
- 每条连接都要独立三次握手 + TLS 握手 + 慢启动,开销大。
- 为了突破 6 条限制,前端搞出域名分片(把资源分散到 img1./img2. 等多个子域)等 hack。
HTTP/1.1 的队头阻塞是**「一条连接同一时刻只能跑一个请求」**造成的应用层排队,本质是协议模型的限制。
三、HTTP/2:多路复用消灭 HTTP 层队头阻塞
HTTP/2 把一条连接逻辑上切成很多并行的「流(Stream)」:
- 流(Stream):一次请求-响应就是一个流,有唯一的 Stream ID。
- 帧(Frame):HTTP/2 传输的最小单位。请求头拆成 HEADERS 帧,响应体拆成一个个 DATA 帧,每个帧头部都标着自己属于哪个 Stream ID。
- 多路复用:不同流的帧可以交错在同一条连接上发送,接收端根据 Stream ID 把帧重新归类、组装成完整的请求/响应。
一条 TCP 连接(HTTP/2):
… [流1-DATA][流3-HEADERS][流1-DATA][流5-DATA][流3-DATA] …
交错传输,接收端按 Stream ID 各自归位
这样,即使流 1 的响应在后端算得慢,流 3、流 5 的帧照样能穿插发回来——应用层的队头阻塞被消除,一条连接就能高效并发,还顺带省掉了多连接的握手开销。HTTP/2 还带了头部压缩(HPACK)、服务器推送、流优先级等特性。
四、致命遗留:HTTP/2 仍卡在 TCP 层
HTTP/2 的多路复用是在 TCP 之上做的,所有流共享同一条 TCP 连接。而 TCP 有一条铁律:向应用交付的字节必须严格按序、不能有洞。
问题就来了:TCP 把这条连接上的所有数据看成一条字节流,它根本不知道上层有「流」的划分。如果某个 TCP 段丢了:
TCP 收到的段: [seg1][seg2][×丢失×][seg4][seg5]
↑ seg4、seg5 已到,但因为 seg3 没到,
TCP 不能交付它们,全压在内核缓冲区等 seg3 重传
即使 seg4、seg5 属于完全不相干的另一个流、而且已经安全抵达,TCP 也不会把它们交给应用——因为按序交付的规矩不允许「跳过 seg3 先给后面的」。于是一个流的一个丢包,冻结了这条连接上所有流的数据交付。这就是 TCP 层队头阻塞。
后果:在丢包率高、网络差的环境(如移动网络、跨国链路),HTTP/2 单连接的这个特性反而成了短板,有时体验不如「多条连接、丢包只影响其中一条」的 HTTP/1.1。
五、HTTP/3:QUIC 从根上解决
要根治 TCP 层队头阻塞,就不能再用 TCP 那套「整条连接一把梭按序交付」的模型。HTTP/3 的做法是换掉 TCP,改用 QUIC——一个跑在 UDP 之上、自己实现可靠传输和拥塞控制的协议。
关键区别:QUIC 在协议内部就有「流」的概念,可靠性是按流独立管理的。
- 每个 QUIC 流有自己独立的序号空间和重传逻辑。
- 流 A 丢了一个包,QUIC 只需重传流 A 的那部分,流 B、流 C 的数据该交付照样交付,完全不受流 A 影响。
QUIC(HTTP/3):
流A:[a1][×a2×][a3] ← a2 丢了,只有流A卡住等重传
流B:[b1][b2][b3] ← 照常交付,不受影响
流C:[c1][c2] ← 照常交付
这样TCP 层的队头阻塞被彻底消除。QUIC 还顺带带来:更快的连接建立(把传输握手和 TLS 握手合并,0-RTT/1-RTT)、连接迁移(用 Connection ID 标识连接,手机从 WiFi 切 4G、IP 变了连接也不断)。
注意:QUIC 消除的是传输层的队头阻塞。如果用了 HTTP 头部压缩(QPACK)且存在跨流依赖,理论上仍可能有极小的「HTTP 层」阻塞,但 QPACK 已针对性设计以规避,工程上可忽略。
六、三代对比总表
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| 传输层 | TCP | TCP | QUIC / UDP |
| 并发方式 | 多条连接串行 | 单连接多路复用 | 单连接多路复用 |
| HTTP 层队头阻塞 | ❌ 有 | ✅ 解决 | ✅ 解决 |
| TCP 层队头阻塞 | ❌ 有 | ❌ 仍有 | ✅ 解决 |
| 头部压缩 | 无 | HPACK | QPACK |
| 连接建立 | TCP + TLS 分开握手 | TCP + TLS | QUIC 握手合并,可 0-RTT |
七、常见误区与追问
| 考点 | 正确口径 |
|---|---|
| TCP 队头阻塞 | 同一字节流前面丢包会阻塞后续字节交付 |
| HTTP/1.1 队头阻塞 | 同连接请求响应顺序限制 |
| HTTP/2 问题 | 多路复用解决应用层排队,但仍受 TCP 丢包阻塞 |
HTTP/2 streams over one TCP connection:
stream 1 frame lost at TCP seq=1000
stream 2 frame seq=1400 arrived
TCP cannot deliver seq=1400 to app until seq=1000 retransmitted
HTTP/2 解决了 HTTP 层队头阻塞,但没有解决 TCP 字节流的队头阻塞;QUIC 才把阻塞粒度降到流级别。
- 误区:HTTP/2 已经彻底消灭队头阻塞。 它消灭了 HTTP/1.1 请求排队,但 TCP 丢包仍会阻塞同连接上的所有流。
- 误区:队头阻塞只发生在 HTTP。 任何有序队列或字节流都可能出现前一个元素阻塞后续元素。
- 误区:多开连接一定最好。 多连接可缓解阻塞,但会增加握手、拥塞控制竞争和资源消耗。
- 追问:QUIC 如何改善? QUIC 在 UDP 上实现多流,某个流丢包通常不阻塞其它流的数据交付。
- 追问:HTTP/1.1 管线化为什么少用? 响应必须按序返回,一个慢响应会挡住后续响应,队头阻塞严重。
- 追问:丢包对 HTTP/2 为什么影响大? 多个流复用一个 TCP 连接,一个 TCP 序号缺口会卡住整个连接的数据交付。
八、加强记忆
队头阻塞就是队头卡住,后面全等。HTTP 里分两层:HTTP 层(HTTP/1.1 一条连接一次只跑一个请求)和 TCP 层(TCP 要求按序交付,一个包丢了后面全等重传)。HTTP/2 用多路复用干掉了 HTTP 层的队头阻塞,但因为还跑在 TCP 上,TCP 层的队头阻塞没解决;HTTP/3 改用 UDP 上的 QUIC,让每个流独立重传,才把 TCP 层的队头阻塞也消除。记一条主线:多路复用治应用层,换掉 TCP 治传输层。