从队头阻塞看 HTTP 变迁史
Contents
[NOTE] Updated August 27, 2023. This article may have outdated content or subject matter.
《 关于队头阻塞(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 的性能思考
对于最佳性能,我们有两个相互冲突的性能优化建议:
- 从 QUIC 的队头阻塞移除中获利:多路复用发送资源(12121212)
- 为了确保浏览器能够尽快处理核心资源:按顺序发送资源(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 没有的主要特性之一,其意义在于:
- 一些可以增量处理/呈现的文件确实从多路复用中获益
- 如果其中一个文件比其他文件小得多,那么它可能会很有用,因为它将更早地下载,而不会对其他文件造成太多的延迟。
- 多路复用允许改变响应的顺序,并为高优先级的响应中断低优先级的响应。
因此,虽然像 12121212 这样的完全“轮询”多路复用很少是您想要的 Web 性能,但是多路复用在总体上绝对是一个有用的特性。