TT Lab
开始
学习 学习路径 课程

实时通信 — WebSocket、gRPC 流式调用与 WebRTC

队头阻塞还留在哪里 — HTTP/3 与 QUIC

在 TT Lab 中继续学习

一句话总结

TCP 为了保证顺序,会扣住丢失的那一个报文段之后的所有字节,而 QUIC 把流下沉到传输层,只让丢失数据所在的那个流等待。

为什么需要它

请回想一下前一个模块实验最后一步看到的数字。设置了一个在下游经过 20,000 字节之后停顿 800ms 的中继器后,承载在同一个连接上的 HTTP/2 的小响应在 900ms 上下结束,而使用独立连接的 HTTP/1.1 的小响应在几十 ms 内就结束了。中继器模拟的是一个报文段的丢失。TCP 只会按顺序把字节流交给应用程序,所以在中间的报文段等待重发期间,在它之后已经到达的字节也被扣在内核缓冲区中。HTTP/2 消除了应用层面的队头阻塞,但其下的 TCP 层面的队头阻塞,反而让所有流都一起承受。

在很少丢包的数据中心内部,这种代价几乎看不出来。但在丢包频繁的移动网络和 Wi-Fi 中,以及像语音这样几十 ms 就能被感知的服务中,最慢的 1% 请求就是由这种停顿填满的。

工作原理

RFC 9000 的 QUIC 运行在 UDP 之上,但 TCP 原先做的事——可靠传输、拥塞控制(RFC 9002)、流量控制——它都重新做了一遍。不同之处在于,流是传输层的概念。一个数据包可以承载多个流的帧,但顺序保证只在流内部进行。如果某个数据包丢失,只有在该数据包中有数据的流要等待重发,其余的流则按到达的顺序交给应用程序。

加密也不是叠加在外面,而是放进了内部。RFC 9001 把 TLS 1.3 握手融入 QUIC 握手,使新连接可以在 1-RTT 内、与之前见过的服务器则可以在 0-RTT 内发送数据。0-RTT 数据会暴露在重放攻击之下,所以只能用于幂等的请求。由于是通过连接 ID 而不是 IP 和端口来识别连接,即使手机从 Wi-Fi 切换到 LTE,连接也可以继续(连接迁移)。这对于像语音通话这样长时间持续的会话,是非常受欢迎的特性。

RFC 9114 的 HTTP/3 在这个 QUIC 之上承载 HTTP 语义。头部压缩也变了。HPACK 假定头部块按发送顺序到达,并据此修改动态表,所以无法用在流各自独立到达的 QUIC 上。因此 RFC 9204 的 QPACK 把表的更新通过单独的编码器流和解码器流发送,并通过设置(SETTINGS_QPACK_BLOCKED_STREAMS)来约定:因为引用了尚未到达的表条目而处于等待的请求流最多允许有几个。

层 HTTP/1.1 HTTP/2 HTTP/3
请求并发 多个连接 一个连接中的流 一个连接中的 QUIC 流
一次丢包的影响 仅该连接 所有流 仅该流
头部压缩 无 HPACK QPACK
加密 可选(TLS) 实际上是 TLS 始终(内置 TLS 1.3)

能否使用 HTTP/3,由服务器通过 Alt-Svc 头或 DNS 的 HTTPS 记录告知,客户端看到之后,会从下一个请求起尝试通过 UDP 连接。

流量控制同样是两层。QUIC 为每个流以及整个连接,分别告知可以接收的最大字节数(MAX_STREAM_DATA、MAX_DATA 帧),同时可以并发打开的流的数量,也通过 MAX_STREAMS 单独告知。HTTP/2 中 SETTINGS 和 WINDOW_UPDATE 所做的事情,下沉到了传输层。所以前面实验中看到的“连接窗口一旦关闭,小响应也要等待”的现象,在 HTTP/3 中同样会发生。QUIC 消除的是丢包造成的队头阻塞,而不是接收方变慢造成的阻塞。

在现场相遇的样子

HTTP/3 并不总是赢家。仍然有封锁 UDP 443 的公司内网和公共 Wi-Fi,所以客户端必须准备好退回 TCP,而且由于在用户空间进行加密和拥塞控制,在相同吞吐量下会多占用 CPU。如果负载均衡器和防火墙不理解 QUIC,连接迁移的优势也会消失。所以大型服务会同时开通两条路径,并依据指标来选择。

本课程的实验 Pod 没有 NET_ADMIN 权限,无法用 tc netem 注入真实的丢包。因此 TCP 一侧用用户空间中继器模拟了丢包的结果(停顿),而 QUIC 则通过这篇理论来讲解。在模拟丢包时,写明模拟了什么、没能模拟什么,也是测量的一部分——中继器并不重现重传定时器和拥塞窗口的缩小。

下一项测验要做什么

区分 TCP 层面与应用层面的队头阻塞,并确认 QUIC 消除了哪一种、如何消除的,以及为什么需要 QPACK。