很多人都在强调 QUIC 能解决 HoL blocking 问题,不好意思,我又要泼冷水了。假设大家都懂 QUIC,不再介绍 QUIC 的细节,直接说问题。
和 TCP 一样,QUIC 也是一个基于连接的,保序的可靠传输协议,TCP 的问题,QUIC 本质上都存在,只是看谁的处理方式更合理更优雅些,不存在彻底解决。QUIC 并没有解决 HoL blocking,只是缓解。里外高低都得先从多路复用开始说。
QUIC 多路复用指的是可以将多条 stream 封装在同一个 QUIC packet 中,或换句话说,多条 stream 可以通过同一条 QUIC connection 承载。
至于为什么非要多路复用,与 HTTP 相关,参考从【这里】开始往后的段落。
若只有 1 条 stream A,QUIC 与 TCP 无异,传输 A1~A100 这 100 个 packet,若 A2 丢了(or 乱序),传输会停滞,receiver 在 hole 未补充前无法交付数据,这就是 HoL blocking。
若一条 QUIC connection 承载 3 条 stream,记为 A,B,C,可有以下典型布局:
第一种布局,只要丢一个 packet,3 条 stream 全部 HoL blocking,第二种,第三种布局下丢一个 packet 只影响一条 stream,但它并不比多条 TCP 好多少,虽确实减少了握手开销,但也带来了问题。第二种情况属于 fair sharing,它延迟了所有 stream,将每条流的传输延时摊派到所有 3 条流的总传输延时,第三种情况属于 run-to-completion,只是简单将 3 条路串行化。
有两种典型丢包模式,随机丢和密集丢,随机丢情况下,按照中心极限定理,所有 stream 同等概率遭遇 HoL blocking,而密集丢情况下,中心极限定理依然起作用,只是影响时间跨度更大。无论如何,采用单条多路复用 QUIC connection 和采用多条 TCP 相比,在传输层面没有任何改变。
TCP 的问题在于它无法获取网络快照,在同样背景信息下,QUIC 也好不到哪里去。 无法预知网络丢包模式,千万不要猜测或假设网络丢包模式,这只会让结果偏离统计期望。采用随机布局是最好的,比如 AABBBACCCBCAABBAAAABCCBBA,这和多条 TCP 有什么不同呢?
QUIC 并没有解决 HoL blocking,只是缓解。但即便是缓解 HoL blocking,QUIC 多路复用也不如 receiver 的 Out-Of-Order Queue 作用更大,ofo Queue 提供了一个 buffer 用来松散保序约束,这才是抓住了本质,若非如此,GBN 才是真正受 HoL blocking 之大害。
此外,实际场景中,stream 数量倾向于少,而丢包乱序倾向于多,这往两边拉,QUIC 解决 HoL blocking 的解释更苍白无力。
那么 QUIC 多路复用的意义到底在哪里?
【这里】我曾说过,QUIC 是从 HTTP 协议演化而来的,当然适配它诞生的水土,HTTP 请求的一个页面上有 jpg,text,javascript 等多种元素,将这些打包在一起再合适不过。QUIC 并不适合传输单个大文件,它简直就是针对 HTTP 的。HTTP/2 提出了二进制分帧,多个资源可以整合在一起传输,但依然沿用了 TCP,整个请求被单一 TCP 连接承载,HoL blocking 问题非常明显。
解决 HTTP/2 HoL blocking 的思路是既然分帧层自己知道多路复用细节,就让分帧层自己处理单独 stream 的 HoL blocking,而不是 TCP 去处理,TCP 不识别 stream,它自身只是一条 stream。就这样,QUIC 诞生了。收到 p2,p3,p4,p5,p8,p10,丢了 p1,p6,p7,p9,对于 TCP,这些数据一个字节也没法交付,全堵在 rcvbuff,而对于 QUIC,也许可交付一部分数据,比如恰好同一个 stream 在收到的 packet 中而没包含在丢失的 packet 中,但也只是也许可以交付,不能保证。
QUIC 只是缓解了 HTTP/2 的 TCP HoL blocking。可为什么不用多条 TCP?为什么不能为每个资源创建一条 TCP 连接?
用进程/线程,协程来解释再恰当不过。指令流是一条需保序可靠执行的串行流。
TCP 和进程/线程一样,是系统感知的,创建一条 TCP 连接,需要握手时间开销,还需要生命周期内连接状态的空间开销,就好比创建一个进程需要生成 PCB 的时间开销,保存 PCB 需要空间开销,都是系统开销,它们的时空复杂度都是 O(n),无论 TCP 连接还是进程/线程,都不可扩展。
因此,为每个请求创建一个进程或线程的 MPM 方案基本都被淘汰了,工人们倾向于在固定数量的进程中处理任意数量的请求,比如 Nginx 就是这种架构。在编程 API 层面上,这就是协程。
协程是系统不感知的,因此也就没有系统开销。QUIC 中的 stream 就是传输协议中协程,QUIC 就是基于 “协-stream” 的多路复用传输协议。
在执行过程中,协程并不比多进程/多线程更快,在很矬的 CPU 上,协程的效果可能还要更差,协程的优势在于资源整合和调度,而非执行效率。与此一致,QUIC 并不比多条 TCP 更快,QUIC 可能确实修正了 TCP 的一些硬伤,比如 SACK blocking 的限制,rwnd 的限制等,但这种 patch 式修正终究改变不了太多,QUIC 的优势亦在资源整合和调度。
我来梳理一下整个故事,TCP 故事里纯背锅。
1997 年 RFC2068 定义的 HTTP/1.1 并没有强制 HTTP 一定要被 TCP 承载:
HTTP communication usually takes place over TCP/IP connections. The default port is TCP 80, but other ports can be used. This does not preclude HTTP from being implemented on top of any other protocol on the Internet, or on other networks. HTTP only presumes a reliable transport; any protocol that provides such guarantees can be used; the mapping of the HTTP/1.1 request and response structures onto the transport data units of the protocol in question is outside the scope of this specification.
注意关键句子 “HTTP only presumes a reliable transport”,彼时除了 TCP 并没有任何成熟的 “reliable transport”(当然,现在也就多了个 QUIC),只好借 TCP 承载 HTTP/1.1。
是 HTTP/1.1 先遭遇了 HoL blocking 而不是 TCP,即时不丢包场景,在 1G 的 text 后跟一个 1K 的 jpg 是一个典型的 HoL blocking case,因为 HTTP Request/Response 必须串行,而 Request 并不知道 text 和 jpg 的大小。如果 Request 知道 jpg 只有 1K,肯定会先请求 jpg,从而解除 HoL blocking。
为 HTTP/1.1 加个二进制分帧层,将文件分片混合传输,就解决了问题,由于此时依然没有除 TCP 外的 reliable transport,加上 HTTP over TCP 已默认成准则,HTTP/2 沿用了 TCP 作为承载协议。这时才遇到 TCP 的 HoL blocking 问题。
缓解(再也不要说解决) TCP HoL blocking 的直接方案就是创建多条 TCP 连接。虽然粒度较粗,但和 “多线程解决单线程 IO 等待的 HoL blocking” 的方法如出一辙。紧接着,出现新的问题,创建多条 TCP 的开销过大,这和多进程,多线程系统开销过大也是同样的嘈点。可以预料,解决问题的思路也一样。
HTTP/3 选择 QUIC 作为传输层,和多条 TCP 连接缓解 HoL blocking 效果相当,但解决了可扩展性问题,消去了 O(n) 增长的 TCP 连接管理开销。
这就是整个故事梗概,TCP 从最开始作为受命托付者,承载 HTTP 20 年,最后却被吐槽,被 QUIC “取而代之”。但实际上,这哪是 TCP 的锅,这是 HTTP 的锅啊。
保序传输本身就是顺序依赖,必须串行解除,这意味着 HoL blocking 是其内秉属性,而非问题。事实上,假如(只是假如)为 HTML 超链接资源加上 size 属性,浏览器便可在少量 TCP 连接上按某种策略 “调度” 资源的 Request,比如 SRPT(Shortest Remaining Processing Time),优先请求最小的资源,就像在 4 个 CPU 上调度 N 个进程一样,总有最优解。
超链接资源支持 size 显然很难,动态获取则需额外一个 RTT,但创建多条 TCP,并行请求多个资源,依然还是调度问题。HTTP 缺的不是一个解决 HoL blocking 问题的传输协议,因为没有这样的协议,HTTP 是个资源池,缺的是调度器。
在公网,QUIC 取代 TCP 的呼声高涨,而老教授 John Ousterhout 也在喷 TCP 不适合 DC。但 TCP 大概率不会消失,无论是 HTTP-Oriented QUIC,or RPC-Oriented Homa,都无法取代 Everything-Oriented TCP,TCP 不假设任何先验,它的意义不是在资源充盈的时候表现得多好,而是最坏的情况下它不至于太糟糕。
当然,QUIC 有很多相对 TCP 的优势,不然它就没有存在的必要了,本文的主旨是,看到不足,才知进步。
HoL blocking 是串行流的内在属性,无论对指令流的执行还是 byte stream 的传输,本质都一样,都是保序流,保序流意味着顺序依赖,且这种顺序只允许唯一一种可能性,降低了容错空间,HoL blocking 对效率的影响显而易见。但经常听到 “QUIC 解决了 HoL blocking” 的说法,需要澄清一下了。最近在读 《布匿战争》,执政官 A 从墨西拿海峡率领 2 个军团纵贯狭长的意大利半岛赶去阿尔卑斯山南麓驰援执政官 B,如何行军最快?列队行军吗?执政官 A 对军团下令,就地解散,个人或结伴自行北上,到达执政官 B 附近后重新列队。这就是乱序传输了,解除了顺序依赖,效率自然高。
浙江温州皮鞋湿,下雨进水不会胖。