作者:wellsjiang,腾讯 CSIG 后台开发工程师
QUIC(Quick UDP Internet Connection)是谷歌推出的一套基于 UDP 的传输协议,它实现了 TCP + HTTPS + HTTP/2 的功能,目的是保证可靠性的同时降低网络延迟。因为 UDP 是一个简单传输协议,基于 UDP 可以摆脱 TCP 传输确认、重传慢启动等因素,建立安全连接只需要一的个往返时间,它还实现了 HTTP/2 多路复用、头部压缩等功能。
众所周知 UDP 比 TCP 传输速度快,TCP 是可靠协议,但是代价是双方确认数据而衍生的一系列消耗。其次 TCP 是系统内核实现的,如果升级 TCP 协议,就得让用户升级系统,这个的门槛比较高,而 QUIC 在 UDP 基础上由客户端自由发挥,只要有服务器能对接就可以。
(图引自《浅谈 QUIC 协议原理与性能分析及部署方案》-by 周陆军)
解决 HTTP1 的一些问题,但是解决不了底层 TCP 协议层面上的队头阻塞问题。2015 年
缺点:HTTP 2
中,多个请求在一个 TCP 管道中的,出现了丢包时,HTTP 2
的表现反倒不如HTTP 1.1
了。因为 TCP 为了保证可靠传输,有个特别的“丢包重传”机制,丢失的包必须要等待重新传输确认,HTTP 2
出现丢包时,整个 TCP 都要开始等待重传,那么就会阻塞该 TCP 连接中的所有请求。而对于 HTTP 1.1
来说,可以开启多个 TCP 连接,出现这种情况反到只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据
HTTP 是建立在 TCP 协议之上,所有 HTTP 协议的瓶颈及其优化技巧都是基于 TCP 协议本身的特性,HTTP2 虽然实现了多路复用,底层 TCP 协议层面上的问题并没有解决(HTTP 2.0 同一域名下只需要使用一个 TCP 连接。但是如果这个连接出现了丢包,会导致整个 TCP 都要开始等待重传,后面的所有数据都被阻塞了),而 HTTP3 的 QUIC 就是为解决 HTTP2 的 TCP 问题而生。
关于 QUIC 的原理,相关介绍的文章很多,这里再列举一下 QUIC 的重要特性。这些特性是 QUIC 得以被广泛应用的关键。不同业务也可以根据业务特点利用 QUIC 的特性去做一些优化。同时,这些特性也是我们去提供 QUIC 服务的切入点。
一条 TCP 连接是由四元组标识的(源 IP,源端口,目的 IP,目的端口)。什么叫连接迁移呢?就是当其中任何一个元素发生变化时,这条连接依然维持着,能够保持业务逻辑不中断。当然这里面主要关注的是客户端的变化,因为客户端不可控并且网络环境经常发生变化,而服务端的 IP 和端口一般都是固定的。
比如大家使用手机在 WIFI 和 4G 移动网络切换时,客户端的 IP 肯定会发生变化,需要重新建立和服务端的 TCP 连接。
又比如大家使用公共 NAT 出口时,有些连接竞争时需要重新绑定端口,导致客户端的端口发生变化,同样需要重新建立 TCP 连接。
所以从 TCP 连接的角度来讲,这个问题是无解的。
当用户的地址发生变化时,如 WIFI 切换到 4G 场景,基于 TCP 的 HTTP 协议无法保持连接的存活。QUIC 基于连接 ID 唯一识别连接。当源地址发生改变时,QUIC 仍然可以保证连接存活和数据正常收发。
那 QUIC 是如何做到连接迁移呢?很简单,QUIC 是基于 UDP 协议的,任何一条 QUIC 连接不再以 IP 及端口四元组标识,而是以一个 64 位的随机数作为 ID 来标识,这样就算 IP 或者端口发生变化时,只要 ID 不变,这条连接依然维持着,上层业务逻辑感知不到变化,不会中断,也就不需要重连。
由于这个 ID 是客户端随机产生的,并且长度有 64 位,所以冲突概率非常低。
(图引自《跟坚哥学 QUIC 系列:连接迁移(Connection Migration)》- by Xiaojian Hong)
以一次简单的浏览器访问为例,在地址栏中输入https://www.abc.com,实际会产生以下动作:
所以,对于数据量小的请求而言,单一次的请求握手就占用了大量的时间,对于用户体验的影响非常大。同时,在用户网络不佳的情况下,RTT 延时会变得较高,极其影响用户体验。
下图对比了 TLS 各版本与场景下的延时对比:
(图引自《QUIC 0-RTT 实现简析及一种分布式的 0-RTT 实现方案》)
从对比我们可以看到,即使用上了 TLS 1.3,精简了握手过程,最快能做到 0-RTT 握手(首次是 1-RTT);但是对用户感知而言, 还要加上 1RTT 的 TCP 握手开销。Google 有提出 Fastopen 的方案来使得 TCP 非首次握手就能附带用户数据,但是由于 TCP 实现僵化,无法升级应用,相关 RFC 到现今都是 experimental 状态。这种分层设计带来的延时,有没有办法进一步降低呢? QUIC 通过合并加密与连接管理解决了这个问题,我们来看看其是如何实现真正意义上的 0-RTT 的握手, 让与 server 进行第一个数据包的交互就能带上用户数据。
QUIC 由于基于 UDP,无需 TCP 连接,在最好情况下,短连接下 QUIC 可以做到 0RTT 开启数据传输。而基于 TCP 的 HTTPS,即使在最好的 TLS1.3 的 early data 下仍然需要 1RTT 开启数据传输。而对于目前线上常见的 TLS1.2 完全握手的情况,则需要 3RTT 开启数据传输。对于 RTT 敏感的业务,QUIC 可以有效的降低连接建立延迟。
究其原因一方面是 TCP 和 TLS 分层设计导致的:分层的设计需要每个逻辑层次分别建立自己的连接状态。另一方面是 TLS 的握手阶段复杂的密钥协商机制导致的。要降低建连耗时,需要从这两方面着手。
QUIC 具体握手过程如下:
(图引自《QUIC 0-RTT 实现简析及一种分布式的 0-RTT 实现方案》)
Quic 使用可插拔的拥塞控制,相较于 TCP,它能提供更丰富的拥塞控制信息。比如对于每一个包,不管是原始包还是重传包,都带有一个新的序列号(seq),这使得 Quic 能够区分 ACK 是重传包还是原始包,从而避免了 TCP 重传模糊的问题。Quic 同时还带有收到数据包与发出 ACK 之间的时延信息。这些信息能够帮助更精确的计算 rtt。此外,Quic 的 ACK Frame 支持 256 个 NACK 区间,相比于 TCP 的 SACK(Selective Acknowledgment)更弹性化,更丰富的信息会让 client 和 server 哪些包已经被对方收到。
QUIC 的传输控制不再依赖内核的拥塞控制算法,而是实现在应用层上,这意味着我们根据不同的业务场景,实现和配置不同的拥塞控制算法以及参数。GOOGLE 提出的 BBR 拥塞控制算法与 CUBIC 是思路完全不一样的算法,在弱网和一定丢包场景,BBR 比 CUBIC 更不敏感,性能也更好。在 QUIC 下我们可以根据业务随意指定拥塞控制算法和参数,甚至同一个业务的不同连接也可以使用不同的拥塞控制算法。
(图引自《TCP BBR - Exploring TCP congestion control》-by Andree Toonk)
虽然 HTTP2 实现了多路复用,但是因为其基于面向字节流的 TCP,因此一旦丢包,将会影响多路复用下的所有请求流。QUIC 基于 UDP,在设计上就解决了队头阻塞问题。
TCP 队头阻塞的主要原因是数据包超时确认或丢失阻塞了当前窗口向右滑动,我们最容易想到的解决队头阻塞的方案是不让超时确认或丢失的数据包将当前窗口阻塞在原地。QUIC 也正是采用上述方案来解决 TCP 队头阻塞问题的。
TCP 为了保证可靠性,使用了基于字节序号的 Sequence Number 及 Ack 来确认消息的有序到达。
(图引自《科普:QUIC 协议原理分析》)
如上图,应用层可以顺利读取 stream1 中的内容,但由于 stream2 中的第三个 segment 发生了丢包,TCP 为了保证数据的可靠性,需要发送端重传第 3 个 segment 才能通知应用层读取接下去的数据。所以即使 stream3 stream4 的内容已顺利抵达,应用层仍然无法读取,只能等待 stream2 中丢失的包进行重传。
在弱网环境下,HTTP2 的队头阻塞问题在用户体验上极为糟糕。
QUIC 同样是一个可靠的协议,它使用 Packet Number 代替了 TCP 的 Sequence Number,并且每个 Packet Number 都严格递增,也就是说就算 Packet N 丢失了,重传的 Packet N 的 Packet Number 已经不是 N,而是一个比 N 大的值,比如 Packet N+M。
QUIC 使用的 Packet Number 单调递增的设计,可以让数据包不再像 TCP 那样必须有序确认,QUIC 支持乱序确认,当数据包 Packet N 丢失后,只要有新的已接收数据包确认,当前窗口就会继续向右滑动。待发送端获知数据包 Packet N 丢失后,会将需要重传的数据包放到待发送队列,重新编号比如数据包 Packet N+M 后重新发送给接收端,对重传数据包的处理跟发送新的数据包类似,这样就不会因为丢包重传将当前窗口阻塞在原地,从而解决了队头阻塞问题。那么,既然重传数据包的 Packet N+M 与丢失数据包的 Packet N 编号并不一致,我们怎么确定这两个数据包的内容一样呢?
QUIC 使用 Stream ID 来标识当前数据流属于哪个资源请求,这同时也是数据包多路复用传输到接收端后能正常组装的依据。重传的数据包 Packet N+M 和丢失的数据包 Packet N 单靠 Stream ID 的比对一致仍然不能判断两个数据包内容一致,还需要再新增一个字段 Stream Offset,标识当前数据包在当前 Stream ID 中的字节偏移量。
有了 Stream Offset 字段信息,属于同一个 Stream ID 的数据包也可以乱序传输了(HTTP/2 中仅靠 Stream ID 标识,要求同属于一个 Stream ID 的数据帧必须有序传输),通过两个数据包的 Stream ID 与 Stream Offset 都一致,就说明这两个数据包的内容一致。
(图引自《科普:QUIC 协议原理分析》)
QUIC 的 packet 除了个别报文比如 PUBLIC_RESET 和 CHLO,所有报文头部都是经过认证的,报文 Body 都是经过加密的。这样只要对 QUIC 报文任何修改,接收端都能够及时发现,有效地降低了安全风险。
如图 3-1 所示,红色部分是 Stream Frame 的报文头部,有认证。绿色部分是报文内容,全部经过加密。
(图引自《科普:QUIC 协议原理分析》)
QUIC 报文的大小需要满足路径 MTU 的大小以避免被分片。当前 QUIC 在 IPV6 下的最大报文长度为 1350,IPV4 下的最大报文长度为 1370。
QUIC 具有众多优点,它融合了 UDP 协议的速度、性能与 TCP 的安全与可靠,同时也解决了 HTTP1、HTTP1.1、HTTP2 中引入的一些缺点,大大优化了互联网传输体验。
腾讯云 CDN 也紧跟技术浪潮,于今年年初迭代中加入了 QUIC 的功能支持,目前正在内测当中。相关介绍以及内测申请可以戳这个链接:
https://cloud.tencent.com/document/product/228/51800