关于队头阻塞(Head-of-Line blocking),看这一篇就足够了》这篇文章对队头阻塞已经讲解的很清楚了,本文从常问问题角度,梳理一下队头阻塞的问题和解决方案。

什么是队头阻塞(Head-of-Line blocking)

简单直接的定义是:

单个(慢)对象阻止其他/后续的对象前进

现实生活中一个很好的比喻就是只有一个收银台的杂货店。一个顾客买了很多东西,最后会耽误排在他后面的人,因为顾客是以先进先出(First In, First Out)的方式服务的。另一个例子是只有单行道的高速公路。在这条路上发生一起车祸,可能会使整个通道堵塞很长一段时间。因此,即使是在“头部(head)”一个单一的问题可以“阻塞(block)”整条“线(line)”。

这个概念一直是最难解决的 Web 性能问题之一。

HTTP/1.1 为什么有队头阻塞

回顾历史, HTTP/1.1 来自一个更简单的时代,协议内容以文本为基础并且在网络上可读。

因为 HTML 中没有办法判断文件大小,所以可能存在大文件传输很久的队头阻塞,解决方法通常是采取多路复用(multiplexing)技术。

但是 HTTP/1.1 是一个纯文本协议,它只在有效荷载(payload)的前面附加头(headers)。它不会进一步区分单个(大块)资源与其他资源。所以它无法将复用的资源区分开来,进而无法实现多路复用。

这是 HTTP/1.1 协议设计方式的一个基础限制。如果只有一个 HTTP/1.1 连接,那么在切换到发送新资源之前,必须完整地传输资源响应。如果前面的资源创建缓慢(例如,从数据库查询动态生成的index.html)或者,如上所述,如果前面的资源很大。这些问题可能会引起队头阻塞问题。

目前 HTTP/1.1 会采用打开多个并行 TCP 连接的方法来解决队头阻塞问题。实践上看,类似于在多个域名上“分片”(sharding)资源的实践(img.mysite.com, static.mysite.com, 等)。这是可行的,但是开学会比较大,对于HTTPS来说还有 TLS 的开销。

总结:

这个问题不能用 HTTP/1.1 解决,而且并行 TCP 连接的补丁解决方案也不能随着时间的推移扩展得太好,很明显需要一种全新的方法,这就是后来的 HTTP/2。

HTTP/2 有什么改进

针对上述提到的 HTTP/1.1 协议无法解决 一个大的或慢的响应会延迟后面的其他响应 的队头阻塞问题,HTTP/2 希望能够正确地复用资源块(resource chunks)。

为了分辨一个块属于哪个资源,或者它在哪里结束,另一个块从哪里开始。HTTP/2 非常优雅地解决了这一问题,它在资源块之前添加了帧(frames)。

HTTP/2 对每个块(chunks)添加了数据帧(DATA frame)。主要包含两个关键的元数据:1. 资源归属 2. 块的大小

协议还有许多其他帧类型,例如头部帧(HEADERS frame)。头部帧也使用(stream id)来指出这些头(headers)属于哪个响应,可以将头(headers)和载荷分离。

HTTP/2 之后要做的是任何调度不同资源来利用有限的带宽。不同优先级的调度方案对web性能也有很大影响。

HTTP/2 为什么有队头阻塞

HTTP/2 已经解决了 HTTP 级别的队头阻塞,但是在网络模型中,TCP也会引发队头阻塞。

HTTP/2 与 TCP 之间的透视图是不匹配的:HTTP/2 可以看到多个独立的资源字节流,而 TCP 只看到一个不透明的字节流。

如果一个 TCP 包丢失,所有后续的包都需要等待它的重传,即使它们包含来自不同流的无关联数据,HTTP/2也不能收包。TCP 具有传输层队头阻塞。

但是 TCP 队头阻塞总的来说还是少见的,更多是突发的连续几个包丢失,可能是由于网络路径中的路由器内存缓冲区暂时溢出引起的。

HTTP/3 有什么改进

TCP本身设计是难以改变的,所以 HTTP/3 将底层传输协议从 TCP 改为基于 UDP 的 QUIC。

QUIC 可以被看作是一个 TCP 2.0。它包括 TCP 的所有特性(可靠性、拥塞控制、流量控制、排序等)的最佳版本,以及更多其他特性。QUIC还完全集成了TLS,并且不允许未加密的连接。

流id(stream id)以前在 HTTP/2 的数据帧(DATA frame)中,现在被下移到传输层的 QUIC 流帧(STREAM frame)中。

QUIC 解决了之前 TCP 的队头阻塞问题。QUIC 在单个资源流中保留了顺序,但不再跨单个流(individual streams)进行排序。

HTTP/3 为什么有队头阻塞

即使在QUIC中,我们仍然有一种队头阻塞的形式:如果在单个流中有一个字节间隙,那么流的后面部分仍然会被阻塞,直到这个间隙被填满。

QUIC 的队头阻塞移除只有在多个资源流同时活动时才有效。这样,如果其中一个流上有包丢失,其他流仍然可以继续。

HTTP/3 的性能思考

对于最佳性能,我们有两个相互冲突的性能优化建议:

  1. 从 QUIC 的队头阻塞移除中获利:多路复用发送资源(12121212)
  2. 为了确保浏览器能够尽快处理核心资源:按顺序发送资源(11112222)

由于丢包问题的不可预测,总的来看多路复用对性能的提升有待考量。

补充

HTTP/1.1 的管道解决了队头阻塞吗

HTTP/1.1 的管道是指浏览器不必等待任何响应数据,现在可以背靠背地发送请求。这样,我们在连接过程中节省了一些 RTTs,使得加载过程更快。

管道解决了请求的队头阻塞,而不是响应的队头阻塞。可悲的是,响应队头阻塞是导致 Web 性能问题最多的原因。

TLS 的队头阻塞

TLS 只能对整个记录进行解密,而TLS记录可能分散在几个TCP包上,任何一个的的丢包可能会引发相应的队头阻塞。

所以 QUIC 集成了 TLS,它不直接使用 TLS 记录,以每个包为基础加密数据。由于 TLS 加密比较复杂,QUIC 在当前实现中仍然会比 TCP 慢。

拥塞控制

拥塞控制器的主要工作是确保网络不会同时被过多的数据过载。

而拥塞控制机制对每个 TCP(和 QUIC)连接都是独立的!这反过来也会影响到 HTTP层 的 Web 性能。

HTTP/1.1同时开6个 TCP 的连接方式有利有弊,一方面每个连接单独根据丢包判断网络拥塞,另一方面6个同时增加会导致更快的进入网络拥塞。

多路复用的意义

HTTP/2 甚至 HTTP/3的拓展在于,多路复用是 HTTP/1.1 没有的主要特性之一,其意义在于:

  1. 一些可以增量处理/呈现的文件确实从多路复用中获益
  2. 如果其中一个文件比其他文件小得多,那么它可能会很有用,因为它将更早地下载,而不会对其他文件造成太多的延迟。
  3. 多路复用允许改变响应的顺序,并为高优先级的响应中断低优先级的响应。

因此,虽然像 12121212 这样的完全“轮询”多路复用很少是您想要的 Web 性能,但是多路复用在总体上绝对是一个有用的特性。