HTTP 队头阻塞

理解 HTTP/1.1 和 HTTP/2 中的队头阻塞问题及解决方案

问题

什么是 HTTP 队头阻塞 (Head-of-Line Blocking)?

解答

队头阻塞是指前面的请求/数据包阻塞了后面的请求/数据包。在 HTTP 协议中,存在两个层面的队头阻塞。

HTTP/1.1 的队头阻塞

HTTP/1.1 默认使用持久连接,但请求必须按顺序发送和接收:

客户端                    服务器
   |                        |
   |------- 请求 A -------->|
   |                        |  (A 处理中...)
   |------- 请求 B -------->|  (B 必须等待)
   |                        |  (A 处理中...)
   |<------ 响应 A ---------|
   |<------ 响应 B ---------|

如果请求 A 处理很慢,请求 B 即使已经准备好也必须等待。

解决方案:

  1. 并发连接 - 浏览器对同一域名开启 6-8 个 TCP 连接
  2. 域名分片 - 将资源分散到多个子域名
  3. HTTP/2 多路复用 - 在单个连接上并行传输多个请求

HTTP/2 的队头阻塞

HTTP/2 通过多路复用解决了应用层的队头阻塞,但 TCP 层仍存在问题:

HTTP/2 帧传输(基于 TCP)

Stream 1: [帧1] [帧2] [帧3]
Stream 2: [帧1] [帧2]
Stream 3: [帧1] [帧2] [帧3]

TCP 传输序列: [1-1][2-1][3-1][1-2][2-2][3-2][1-3][3-3]

                    如果这个包丢失
                    后面所有包都要等待重传
// 模拟 TCP 队头阻塞的影响
const packets = ['1-1', '2-1', '3-1', '1-2', '2-2', '3-2'];
const lostPacket = '3-1';

// TCP 必须按序交付,丢包会阻塞所有后续数据
function tcpDeliver(packets, lostIndex) {
  const delivered = [];
  const blocked = [];
  
  for (let i = 0; i < packets.length; i++) {
    if (i < lostIndex) {
      delivered.push(packets[i]);
    } else {
      // 丢包后,所有包都被阻塞,等待重传
      blocked.push(packets[i]);
    }
  }
  
  return { delivered, blocked };
}

console.log(tcpDeliver(packets, 2));
// { delivered: ['1-1', '2-1'], blocked: ['3-1', '1-2', '2-2', '3-2'] }

HTTP/3 的解决方案

HTTP/3 使用 QUIC 协议(基于 UDP),每个流独立处理丢包:

QUIC 传输

Stream 1: [帧1] [帧2] [帧3]  → 独立重传
Stream 2: [帧1] [帧2]        → 不受影响
Stream 3: [帧1] ✗ [帧3]      → Stream 3 等待重传,其他流继续
// QUIC 的独立流处理
const streams = {
  stream1: ['帧1', '帧2', '帧3'],
  stream2: ['帧1', '帧2'],
  stream3: ['帧1', '帧2', '帧3']
};

// Stream 3 的帧2 丢失
function quicDeliver(streams, lostStream, lostFrame) {
  const result = {};
  
  for (const [stream, frames] of Object.entries(streams)) {
    if (stream === lostStream) {
      // 只有丢包的流被阻塞
      result[stream] = {
        delivered: frames.slice(0, lostFrame),
        status: '等待重传'
      };
    } else {
      // 其他流正常传输
      result[stream] = {
        delivered: frames,
        status: '完成'
      };
    }
  }
  
  return result;
}

console.log(quicDeliver(streams, 'stream3', 1));
// stream1: { delivered: ['帧1', '帧2', '帧3'], status: '完成' }
// stream2: { delivered: ['帧1', '帧2'], status: '完成' }
// stream3: { delivered: ['帧1'], status: '等待重传' }

对比总结

协议应用层队头阻塞传输层队头阻塞
HTTP/1.1✗ 存在✗ 存在
HTTP/2✓ 解决✗ 存在
HTTP/3✓ 解决✓ 解决

关键点

  • HTTP/1.1 队头阻塞:请求必须按序处理,前面的请求阻塞后面的
  • HTTP/2 多路复用解决了应用层队头阻塞,但 TCP 丢包仍会阻塞所有流
  • HTTP/3 使用 QUIC 协议,每个流独立处理丢包,彻底解决队头阻塞
  • 浏览器通过并发连接(6-8个)缓解 HTTP/1.1 的队头阻塞
  • 高丢包网络环境下,HTTP/2 性能可能不如 HTTP/1.1 的多连接方案