最近一直在实习,好久没有输出新的知识了。
今天抽空把最近看的 QUIC 的笔记做一个小小的整理吧。
QUIC 协议
一、QUIC协议简介
Quic 全称 quick udp internet connection,“快速 UDP 互联网连接”,(和英文 quick 谐音,简称“快”)是由 Google 提出的使用 UDP 进行多路并发传输的协议。
QUIC协议模型如下图,它向下使用了操作系统提供的 UDP 套接字,向上为应用层协议(例如:HTTP/2)提供了可靠且安全的传输通道。虽然QUIC 在实现上基于传输层协议 UDP,但是它在协议设计上并没有依赖于 UDP 的特性,即并没有使用 UDP 端口来表示一条传输层连接。QUIC使用UDP的目的仅仅是为了保持和现有网络的兼容性,因为目前互联网上的某些防火墙会屏蔽 TCP 和 UDP 之外的传输协议,因此,尽管 QUIC 工作于传输层协议UDP之上,研究员仍然普遍将它归类为传输层协议。
为了提供对移动性的支持,QUIC 放弃了 TCP/IP 网络中使用五元组(源 IP,源端口,目的IP,目的端口,协议标识符)来唯一标识一条连接的方式,而是使用一个全局唯一的随机生成的ID(即 连接 ID )来标识一条连接。这样,当通信一方的物理网络发生变化时,例如从蜂窝网络切换到 WIFI 网络,在原先网络中建立的 QUIC 连接就可以无缝迁移到新的网络下,从而保证网络服务在用户切换网络的过程中不被打断。
二、QUIC的特性
1. 建立连接时延低
QUIC设计了自己的握手协议,并达到了比 TCP+TLS 更低的握手延时。TLS 使用了对称加密和非对称加密相结合的方式为数据的安全性提 供保障。其运行机制可以简单分为2步:
客户端与服务端通过握手协议生成共享密钥;
双方分别使用共享密钥进行数据的加密和解密。
在 TLS 1.2 中,握手协议通过 RSA 算法或者迪菲赫尔曼(D-H)算法完成。其中 RSA 要求通信双方首先交换各自的 RSA 公钥,再通过 RSA 加密传输一个新生成的共享密钥,因此完成握手需要 2个往返时间。而 TLS 1.3 ,迪菲赫尔曼成为唯一的密钥交换协议,通信双方可以直接通过对方的公钥和自己的私钥生成共享密钥,握手延时也从原来的2个 RTT 降为了1个 RTT。
下图展示了QUIC 和 TCP 握手延时的区别
目前使用最为广泛的协议组是 TCP+TLS/1.2,在该情况下,客户端与服务端之间的握手至少需要消耗3个 RTT,包括1个 RTT 的 TCP 握手延时和2个RTT 的 TLS握手延时。在最新的 TCP + TLS/1.3 协议组中,因为 TLS 的握手延时由原来的2个 RTT 减小到了1个 RTT,所以总的握手延时降为了2个RTT。QUIC在此基础上更进一步地进行了优化,它允许在建立传输层连接的同时进行 TLS 握手。也就是说,QUIC 的握手请求数据包中也包含了 TLS/1.3 的握手请求数据,从而使整体的握手延时降为了1个RTT 。我们称 QUIC 的这种握手方式为1-RTT 握手。
如果客户端在之前已经连接过服务端,客户端和服务端之间则会保留上次会话的相关加密信息,重用共享密钥。在这种情况下,客户端可以直接使用上次通信的共享密钥进行数据加密,而新的密钥交换过程会与数据传输同步进行。也就是说,数据传输不需要等待握手的完成,从而实现没有任何延时的握手,我们称 QUIC/TLS 1.3 的 这 种 握 手 方 式 为 0-RTT 握手。
0-RTT 握手极大地降低了客户端的握手延时从而可以显著减少用户访问网络的延迟感,提升用户体验。
2. 拥塞控制协议可拔插
什么叫可插拔呢?就是能够非常灵活地生效,变更和停止。体现在如下方面:
- 应用程序层面就能实现不同的拥塞控制算法,不需要操作系统,不需要内核支持。这是一个飞跃,因为传统的 TCP 拥塞控制,必须要端到端的网络协议栈支持,才能实现控制效果。而内核和操作系统的部署成本非常高,升级周期很长,这在产品快速迭代,网络爆炸式增长的今天,显然有点满足不了需求。而QUIC协议将拥塞控制算法移动到了两个端点的用户空间,而不是内核空间。
- 即使是单个应用程序的不同连接也能支持配置不同的拥塞控制。就算是一台服务器,接入的用户网络环境也千差万别,结合大数据及人工智能处理,我们能为各个用户提供不同的但又更加精准更加有效的拥塞控制。比如 BBR 适合,Cubic 适合。
- 应用程序不需要停机和升级就能实现拥塞控制的变更,我们在服务端只需要修改一下配置,Reload 一下,完全不需要停止服务就能实现拥塞控制的切换。
3. 单调递增的 Packet Number
TCP 为了保证可靠性,使用了基于字节序号的 Sequence Number 及 Ack 来确认消息的有序到达。
QUIC 同样是一个可靠的协议,它使用 Packet Number 代替了 TCP 的 sequence number,并且每个 Packet Number 都严格递增,也就是说就算 Packet N 丢失了,重传的 Packet N 的 Packet Number 已经不是 N,而是一个比 N 大的值。而 TCP 呢,重传 segment 的 sequence number 和原始的 segment 的 Sequence Number 保持不变,也正是由于这个特性,引入了 TCP 重传的歧义问题。
如上图所示,超时事件 RTO 发生后,客户端发起重传,然后接收到了 Ack 数据。由于序列号一样,这个 Ack 数据到底是原始请求的响应还是重传请求的响应呢?不好判断。
如果算成原始请求的响应,但实际上是重传请求的响应(上图左),会导致采样 RTT 变大。如果算成重传请求的响应,但实际上是原始请求的响应,又很容易导致采样 RTT 过小。
由于 Quic 重传的 Packet 和原始 Packet 的 Pakcet Number 是严格递增的,所以很容易就解决了这个问题。
如上图所示,RTO 发生后,根据重传的 Packet Number 就能确定精确的 RTT 计算。如果 ACK 的 Packet Number 是 N+M,就根据重传请求计算采样 RTT。如果 ACK 的 Pakcet Number 是 N,就根据原始请求的时间计算采样 RTT,没有歧义性。
但是单纯依靠严格递增的 Packet Number 肯定是无法保证数据的顺序性和可靠性。QUIC 又引入了一个 Stream Offset 的概念。
即一个 Stream 可以经过多个 Packet 传输,Packet Number 严格递增,没有依赖。但是 Packet 里的 Payload 如果是 Stream 的话,就需要依靠 Stream 的 Offset 来保证应用数据的顺序。如错误! 未找到引用源。所示,发送端先后发送了 Pakcet N 和 Pakcet N+1,Stream 的 Offset 分别是 x 和 x + y。
假设 Packet N 丢失了,发起重传,重传的 Packet Number 是 N+2,但是它的 Stream 的 Offset 依然是 x,这样就算 Packet N + 2 是后到的,依然可以将 Stream x 和 Stream x+y 按照顺序组织起来,交给应用程序处理。
4. 基于 stream 和 connecton 级别的流量控制
QUIC 的流量控制类似 HTTP/2,即在 Connection 和 Stream 级别提供了两种流量控制。
为什么需要两类流量控制呢?主要是因为 QUIC 支持多路复用。
- Stream 可以认为就是一条 HTTP 请求。
- Connection 可以类比一条 TCP 连接。多路复用意味着在一条 Connetion 上会同时存在多条 Stream。既需要对单个 Stream 进行控制,又需要针对所有 Stream 进行总体控制。
QUIC 实现流量控制的原理比较简单:
通过 window_update 帧告诉对端自己可以接收的字节数,这样发送方就不会发送超过这个数量的数据。
通过 BlockFrame 告诉对端由于流量控制被阻塞了,无法发送数据。
QUIC 的流量控制和 TCP 有点区别,TCP 为了保证可靠性,窗口左边沿向右滑动时的长度取决于已经确认的字节数。如果中间出现丢包,就算接收到了更大序号的 Segment,窗口也无法超过这个序列号。
但 QUIC 不同,就算此前有些 packet 没有接收到,它的滑动只取决于接收到的最大偏移字节数。
针对 Stream:
可用窗口 = 最大窗口数 - 接收到的最大偏移数
针对 Connection:
可用窗口 = stream1 可用窗口 + Stream2 可用窗口 + ….. + Steam N 可用窗口
最重要的是,我们可以在内存不足或者上游处理性能出现问题时,通过流量控制来限制传输速率,保障服务可用性。
5. 没有队头阻塞的多路复用
QUIC 的多路复用和 HTTP2 类似。在一条 QUIC 连接上可以并发发送多个 HTTP 请求 (stream)。但是 QUIC 的多路复用相比 HTTP2 有一个很大的优势。
QUIC 一个连接上的多个 stream 之间没有依赖。这样假如 stream2 丢了一个 udp packet,也只会影响 stream2 的处理。不会影响 stream2 之前及之后的 stream 的处理。这也就在很大程度上缓解甚至消除了队头阻塞的影响。
6. 连接迁移
一条 TCP 连接是由四元组标识的【源 IP,源端口,目的 IP,目的端口】。
什么叫连接迁移呢?
- 就是当其中任何一个元素发生变化时,这条连接依然维持着,能够保持业务逻辑不中断。当然这里面主要关注的是客户端的变化,因为客户端不可控并且网络环境经常发生变化,而服务端的 IP 和端口一般都是固定的。
比如我们使用手机在 WIFI 和 4G 移动网络切换时,客户端的 IP 肯定会发生变化,需要重新建立和服务端的 TCP 连接。
又比如大家使用公共 NAT 出口时,有些连接竞争时需要重新绑定端口,导致客户端的端口发生变化,同样需要重新建立 TCP 连接。
针对 TCP 的连接变化,MPTCP 其实已经有了解决方案,但是由于 MPTCP 需要操作系统及网络协议栈支持,部署阻力非常大,目前并不适用。所以从 TCP 连接的角度来讲,这个问题是无解的。
那 QUIC 是如何做到连接迁移呢?
- 很简单,任何一条 QUIC 连接不再以 IP 及端口四元组标识,而是以一个 64 位的随机数作为 ID 来标识,这样就算 IP 或者端口发生变化时,只要 ID 不变,这条连接依然维持着,上层业务逻辑感知不到变化,不会中断,也就不需要重连。由于这个 ID 是客户端随机产生的,并且长度有 64 位,所以冲突概率非常低。
三、QUIC的应用和前景
QUIC 协议虽然是基于UDP 来实现的,但是它将 TCP 的重要功能都进行了延续和优化,否则使用者是不会买账的。QUIC 协议的核心思想是将 TCP 协议在内核实现的诸如可靠传输、流量控制和拥塞控制等功能转移到用户态来实现,同时在加密传输方向的尝试也推动 TLS/1.3 的发展。
QUIC在用户态实现,而不是在内核中实现。当数据在应用程序之间移动时,这通常会由于上下文切换而调用额外的开销。 但是在QUIC下协议栈旨在由单个应用程序使用,每个应用程序使用QUIC在UDP上托管自己的连接。最终差异可能非常小,因为整个HTTP/2堆栈的大部分已经存在于应用程序(或更常见的库)中。 将剩余部分放在这些库中,基本上是纠错,对 HTTP/2 堆栈的大小或整体复杂性几乎没有影响。
QUIC允许更容易地进行未来更改,因为它不需要更改内核就可以进行更新。
QUIC的长期目标之一是添加前向纠错和改进的拥塞控制。
<—-参考连接—->