本文目录
- 1.前言
- 2. HTTP缺点
- 缺点一:建立连接的握手延迟大
- 缺点二:多路复用的队首阻塞
- 缺点三:TCP协议的更新滞后
- 3.TCP缺点
- 3.QUIC
- 优点一:避免队首阻塞的多路复用
- 优点二:支持连接迁移
- 优点三:可插拔的拥塞控制
- 优点四:前向纠错(FEC)
- 4.QUIC协议原理分析
- 参考资料
1.前言
在三十年前,我们见证了显卡和网卡作为CPU的辅助外设的时代。然而,随着技术的发展,这些外设逐渐演变成了核心组件,GPU和SmartNIC现在在某些应用场景中扮演着类似CPU的角色。这种转变反映了硬件技术的进步和应用需求的变化。
类似地,TCP协议在它诞生之初,由于内存和带宽的昂贵,设计时更注重空间的优化而不是时间。GBN(Go-Back-N)协议就是这种设计理念的产物,它的TCP头部设计在当时看来是合理的。但随着时间推移,当内存和带宽变得相对廉价,人们开始追求更高的时间效率,TCP的某些限制,如16位的端口号,开始显得力不从心。
在大并发场景下,TCP的这些设计选择导致了一些问题,比如bind操作占用CPU资源过高。尽管有人提出采用TLV(Type-Length-Value)格式可能会更优,但在TCP诞生的时代,这种设计并不现实。
随着技术的发展,人们开始寻求新的解决方案,意味着我们需要新的传输协议来适应现代网络的需求。QUIC协议应运而生,它是一个由Google提出的基于UDP的协议,因其高效的传输效率和优秀的多路并发能力,被选为下一代互联网协议HTTP/3的底层传输协议。
QUIC不仅在Web领域展现出其优势,它的低延迟和高吞吐特性同样适用于其他需要这些特性的通用传输场景。它代表了一种从传统TCP协议向更现代、更高效的协议转变的趋势。
如果追求更快的陆地交通方式,我们会选择乘坐高铁而不是老旧的绿皮车,网络协议的选择也应该与时俱进。TCP的优化可能已经接近极限,而像QUIC这样的新协议,或者将来可能出现的更优秀的协议,将会引领我们进入一个更高效、更快速的网络传输时代。
众所周知,HTTP从最初的HTTP/0.9,经历了HTTP/1.x,HTTP/2到最新的HTTP/3这几个大的更新版本。在HTTP/3版本之前,HTTP底层都是用TCP传输数据,而伴随着移动互联网的发展,网络交互场景越来越丰富并要求及时性,传统TCP固有的性能瓶颈越来越不能满足需求。
2. HTTP缺点
缺点一:建立连接的握手延迟大
HTTPS包含两个握手:1)TCP三次握手,1个RTT;2)TLS握手,2个RTT。完整握手总共需要3个RTT,对于直播等需要首帧秒开场景,握手延迟太大。
TCP三次握手的步骤(准确来说是1.5个RTT)
- 客户端发送SYN(同步序列编号):客户端向服务器发送一个SYN(同步序列编号)标志位的TCP段,请求建立连接。这个SYN段包含客户端的初始序列号(seq=x)。
- 服务器响应SYN+ACK(同步+确认):服务器收到客户端的SYN后,如果同意建立连接,则会发送一个SYN+ACK响应,其中包含服务器的初始序列号(seq=y)和对客户端序列号的确认(ack=x+1)。
- 客户端发送ACK(确认):客户端收到服务器的SYN+ACK后,会发送一个ACK确认,确认号为服务器的序列号加1(ack=y+1),这样连接就建立起来了。
传统的TLS握手的步骤
- 客户端Hello:
○ 客户端发送一个TLS ClientHello消息给服务器,该消息包含客户端支持的TLS版本、一个客户端生成的随机数(Client Random,通常称为client_nonce),以及客户端支持的加密套件列表和压缩算法列表。 - 服务器Hello:
○ 服务器响应一个TLS ServerHello消息,该消息包含服务器选择的TLS版本、一个服务器生成的随机数(Server Random,通常称为server_nonce),以及基于客户端提供的列表选择的加密套件和压缩算法。 - 证书和密钥交换:
○ 证书:服务器发送其证书给客户端,证书中包含了服务器的公钥,通常由受信任的证书颁发机构(CA)签发。
○ 密钥交换:服务器和客户端使用所选的密钥交换算法来交换密钥生成参数。例如,在RSA密钥交换中,服务器发送一个用其私钥加密的pre-master secret到客户端。客户端使用服务器的公钥解密得到pre-master secret,然后使用它来生成master secret。 - 服务器Hello Done:
○ 服务器发送一个ServerHelloDone消息,表明其握手状态的结束,此时客户端可以开始发送加密信息。 - 客户端密钥交换:
○ 客户端使用协商的密钥交换算法发送一个消息,通常包含一个用pre-master secret派生出的密钥(Client Key Exchange)。 - 更改密码规范指示器:
○ 客户端发送一个ChangeCipherSpec消息,表明后续的消息都将使用之前协商的密钥进行加密。 - 客户端结束握手消息:
○ 客户端发送一个Finished消息,该消息是一个验证握手过程完整性的哈希值,包含了之前握手消息的所有内容。 - 服务器更改密码规范指示器:
○ 服务器响应一个ChangeCipherSpec消息,表明它也将使用协商的密钥进行加密。 - 服务器结束握手消息:
○ 服务器发送自己的Finished消息,也是一个包含握手消息哈希值的验证消息。 - 应用数据传输:
○ 握手完成后,客户端和服务器开始使用协商的加密参数来加密和解密传输的数据。
缺点二:多路复用的队首阻塞
以HTTP/2多路复用为例,多个数据请求作为不同的流,共用一条TCP连接发送,所有的流应用层都必须按序处理。若某个流的数据丢失,后面其他流的数据都会被阻塞,直到丢失的流数据重传完成其他流才能被继续传输。即使接收端已经收到之后流的数据包,HTTP协议也不会通知应用层去处理。
缺点三:TCP协议的更新滞后
TCP协议是实现在操作系统内核内,但是用户端的操作系统版本升级非常困难,很多老旧的系统像WindowsXP还有大量用户使用,因此TCP协议的一些更新很难被快速推广。
正是考虑到以上的这些问题,QUIC在应用层之上基于UDP实现丢包恢复,拥塞控制,加解密,多路复用等功能,既能优化握手延迟,同时又完全解决内核协议更新滞后问题。
3.TCP缺点
TCP如何优化吞吐率?会不会很难优化?为什么难优化?为什么优化不了?因为滑动窗口。
在讨论网络传输性能时,经常会遇到关于拥塞窗口(Congestion Window, cwnd)限制的疑问。我的意思是,cwnd限制的情况通常有两种原因:
- 拥塞控制算法的准确性:如果你的拥塞控制(Congestion Control, cc)算法计算准确,那么cwnd限制可能意味着网络实际上正在经历拥塞。
- 拥塞控制算法的不准确性:如果你的拥塞控制算法计算不准确,那么cwnd限制可能是由于算法本身的问题导致的。
换句话说,拥塞窗口(cwnd)确定了TCP连接的最小吞吐量,确保了数据传输的一个基本性能水平,但它并不对吞吐量的上限施加限制。这意味着,尽管cwnd为TCP连接提供了一个性能的下限保障,但实际的吞吐量仍有可能超过这个下限,特别是在网络条件良好且拥塞控制算法有效的情况下。
通过这种方式,我们可以更好地理解cwnd在TCP流量控制中的作用,以及它如何影响网络传输的性能。
● 为什么因为滑动窗口?因为滑动窗口本质上是一个“停-等”策略。
● “停-等”怎么了?因为要等,所以要停,一停就慢了(也叫HoL阻塞)。
队头阻塞(英語:Head-of-line blocking,缩写:HOL blocking)在计算机网络的范畴中是一种性能受限的现象。它的原因是一列的第一个数据包(队头)受阻而导致整列数据包受阻。例如它有可能在缓存式输入的交换机中出现,有可能因为传输顺序错乱而出现,亦有可能在HTTP流水线中有多个请求的情况下出现。
● 等什么?等buffer被填满。
● 什么buffer?就是接收窗口。
● 等接收窗口被填满有什么问题吗?问题就在这里!
● 那么要取消滑动窗口吗?这个最后再谈。先说本质问题。
● 问题在于,这块作为滑动窗口的buffer有一个约束,要求其中的字节序列号必须连续。这意味着这个buffer便不再是一块无差别随机访问的内存了,而变成了一个队列。一旦出现一个hole,传输过程就必须停下来等待它被填充,期间窗口的滑动是被阻滞。
诞生于1970~1980年代的TCP从来就不是为性能而生的。当人们意识到性能问题的时候,随即就出现了依赖out-of-order queue的selective ACK(选择性确认(Selective Acknowledgment, SACK)应运而生,它依赖于乱序队列(out-of-order queue)来提高效率)。但于本质问题无补,基因决定了上限。
尽管SACK能够在一定程度上改善TCP对丢包的处理,它并不能完全解决TCP的固有性能限制。TCP的设计基因,即其保守的拥塞控制和流量控制机制,已经为其性能的上限设定了边界。这些机制在避免网络拥塞和确保数据包可靠传输方面发挥了关键作用,但同时也牺牲了一定的传输效率。
在设计网络传输协议时,明确区分端到端语义和传输语义是至关重要的,这样做可以确保网络架构的分层模型得以有效运作。分层模型,如OSI模型或TCP/IP模型,通过将网络通信的不同功能划分为独立的层次,提高了设计的灵活性和可维护性。
- 端到端语义:端到端语义关注的是应用程序间的通信,它负责确保数据的顺序和完整性。在端到端层面,协议只看到字节流,而不关心这些字节是如何被分割成数据包进行传输的。
- 传输语义:传输语义则专注于在网络中移动数据包,处理数据包的分割、传输和重组。传输层协议如TCP或UDP,对数据包进行编号和传输,但它们不负责字节流的顺序。
- 映射过程:为了使这两个语义能够协同工作,协议设计中需要在连续的字节流和离散的数据包之间建立映射关系。这意味着传输层需要在内部管理序列号空间,以确保数据包能够被正确地排序和重组。
在设计传输协议时,一个核心原则是将端到端的语义与传输语义区分开来,这是网络分层模型的基础。简单来说,传输层关注数据包的传输,而不涉及数据包内部的序列号;而端到端层则负责字节流的顺序重组,而不关心数据包的边界。实现这种分离需要在字节流的序列号空间和数据包之间建立映射。
允许乱序是分组交换的基本特征之一,TCP协议也因此将保序逻辑放在了端到端,然而对于传输逻辑,TCP依然更像一条虚电路流,而不像数据报分组。(这是TCP后续停-等buffer,GBN低效的根源。为什么呢?)
整个传输逻辑的实体不是TCP数据包,而是没有边界的TCP字节流。换句话说,发送端一次发送1000字节的数据,到了接收端,可能收到2个数据包,一个20字节,另一个980字节,也可能收到1000个数据包,每一个1字节,当然,更大可能是依然是一个1000字节的数据包。无论如何,接收端无法“基于数据包做ACK,只能对字节做ACK”,但主机处理处理依然以数据包为单位。这逆转了人们设计协议时对时间和空间做trade-off(权衡)时的偏好。
对空间的关注超过了对吞吐的关注。为每一个字节安排一个ACK,将使TCP协议头变得冗长到不可接受,这不仅不能带来吞吐的提升,还会消耗大量的带宽用于ACK本身的管理开销。
摒弃数据包的概念,直接对TCP的字节流对积累ACK可以解决上述问题。积累ACK自然而然就等价于滑动窗口和GBN了。
流式抽象导致了必须基于字节进行ACK,进而积累确认和GBN重传被认为是性价比最高的。这是一个很低效的设计,但管用。
● 接收窗口限制了可用buffer的大小,而buffer只有够不够的问题,如果空间不足,显式发送抑制消息给ACK逻辑即可,ACK将抑制消息反馈到源端。
● 与buffer的解释类似,网络传输只有带宽满不满,拥塞与否的问题,而这些完全通过congestion control,pacing逻辑等完成,令牌桶里完全序列号的概念。
端到端语义和传输语义分离的好处不胜枚举:
- 很容易统计重传率指标,需要传输的字节在端到端子层,实际传输的字节在传输子层。
- 网络传输可以肆意并行,无需关注乱序,重组等问题,以最大化带宽利用率为目标。
- 确认数据包而不是确认字节,灵活实现积累ACK或selective ACK,ACK报文可压缩,可编码。
滑动窗口本身的问题。要不要取消它?滑动窗口要取消,问题在于,首先,如何做流量控制,其次,流量控制如何使用buffer。
● 第一个问题很容易:
- buffer是标量而非矢量,只看大小不看方向,只要尚未超过最大使用限额,就可以收数据。
- 设置可用buffer最大值以及最大允许的突发,超过限额即发送源抑制。
看第二个问题。如果buffer中数据有hole不连续,应用程序如何收走数据?如果数据一直不被收走,buffer总会没有空闲空间。这不是又回到问题的原点了吗?问题的解决在于使用buffer的方式。
采用子连接复用的方式让多个连接共享同一个buffer:buffer粒度更细,施展空间更大,同一子连接HoL阻塞概率更低。
3.QUIC
本质上QUIC协议位于传输层和应用层之间,通常被称为“应用层的TCP”。这是因为QUIC在UDP的基础上增加了类似于TCP的可靠性、拥塞控制和安全性特性,同时为应用层提供了一些额外的功能,如:
- 更快的连接建立:QUIC使用会话复用来减少连接建立的延迟,允许在一个新的连接上复用之前连接的参数,从而避免了TLS握手的等待时间。
- 多路复用:QUIC支持在同一连接上并行传输多个数据流,而不会像TCP那样因一个数据流的延迟而阻塞其他数据流。
- 安全性:QUIC内置了类似于TLS的加密,提供了端到端的安全连接,保护数据传输免受窃听和篡改。
- 连接迁移:QUIC允许在不中断服务的情况下进行连接迁移,例如,当用户从一个网络切换到另一个网络时。
QUIC是一种创新的传输层协议,它在建立连接的时间上进行了显著优化,通常在0到1个往返时间(RTT)内完成。以下是QUIC协议优化的两个关键方面:
- 使用UDP减少延迟:QUIC协议基于UDP构建,避免了TCP所需的三次握手过程,从而减少了通常需要1.5个RTT的建立连接的延迟。
- TLS 1.3的高效握手:QUIC采用了TLS协议的最新版本TLS 1.3,与之前的TLS 1.1和1.2相比,TLS 1.3允许客户端在TLS握手完成之前就开始发送应用数据。这种设计支持1 RTT的快速握手,甚至在某些情况下可以实现0 RTT的连接建立。
对于首次建立连接的客户端,QUIC的握手协商过程需要1个RTT。然而,对于已经建立过连接的客户端,QUIC能够利用之前协商并缓存的信息来快速恢复TLS连接,这个过程只需要0个RTT。因此,QUIC的连接建立时间大多数情况下接近0 RTT,极少数情况下为1 RTT。与需要3个RTT才能建立连接的HTTPS相比,QUIC在连接速度上具有明显的优势,这对于需要快速响应的应用场景(如实时通信和在线游戏)尤其重要。
优点一:避免队首阻塞的多路复用
QUIC同样支持多路复用,相比HTTP/2,QUIC的流与流之间完全隔离的,互相没有时序依赖。如果某个流出现丢包,不会阻塞其他流数据的传输和应用层处理,所以这个方案并不会造成队首阻塞。
优点二:支持连接迁移
什么是连接迁移?举个例子,当你用手机使用蜂窝网络参加远程会议,当你把网络切换到WLAN时,会议客户端会立马重连,视频同时出现一瞬间的卡顿。这是因为,TCP采用四元组(包括源IP、源端口、目标地址、目标端口)标识一个连接,在网络切换时,客户端的IP发生变化,TCP连接被瞬间切断然后重连。连接迁移就是当四元组中任一值发生变化时,连接依旧能保持,不中断业务。QUIC支持连接迁移,它用一个(一般是64位随机数)ConnectionID标识连接,这样即使源的IP或端口发生变化,只要ConnectionID一致,连接都可以保持,不会发生切断重连。
优点三:可插拔的拥塞控制
QUIC是应用层协议,用户可以插拔式选择像Cubic、BBR、Reno等拥塞控制算法,也可以根据具体的场景定制私有算法。
优点四:前向纠错(FEC)
QUIC支持前向纠错,弱网丢包环境下,动态的增加一些FEC数据包,可以减少重传次数,提升传输效率。
4.QUIC协议原理分析
从上个世纪 90 年代互联网开始兴起一直到现在,大部分的互联网流量传输只使用了几个网络协议。使用 IPv4 进行路由,使用 TCP 进行连接层面的流量控制,使用 SSL/TLS 协议实现传输安全,使用 DNS 进行域名解析,使用 HTTP 进行应用数据的传输。
而且近三十年来,这几个协议的发展都非常缓慢。TCP 主要是拥塞控制算法的改进,SSL/TLS 基本上停留在原地,几个小版本的改动主要是密码套件的升级,TLS1.3[3] 是一个飞跃式的变化,但截止到今天,还没有正式发布。IPv4 虽然有一个大的进步,实现了 IPv6,DNS 也增加了一个安全的 DNSSEC,但和 IPv6 一样,部署进度较慢。
一方面是历史悠久使用广泛的古老协议,另外一方面用户的使用场景对传输性能的要求又越来越高。如下几个由来已久的问题和矛盾就变得越来越突出。
- 协议历史悠久导致中间设备僵化。
- 依赖于操作系统的实现导致协议本身僵化。
- 建立连接的握手延迟大。
- 队头阻塞。
中间设备的僵化
可能是 TCP 协议使用得太久,也非常可靠。所以我们很多中间设备,包括防火墙、NAT 网关,整流器等出现了一些约定俗成的动作。
比如有些防火墙只允许通过 80 和 443,不放通其他端口。NAT 网关在转换网络地址时重写传输层的头部,有可能导致双方无法使用新的传输格式。整流器和中间代理有时候出于安全的需要,会删除一些它们不认识的选项字段。
TCP 协议本来是支持端口、选项及特性的增加和修改。但是由于 TCP 协议和知名端口及选项使用的历史太悠久,中间设备已经依赖于这些潜规则,所以对这些内容的修改很容易遭到中间环节的干扰而失败。
而这些干扰,也导致很多在 TCP 协议上的优化变得小心谨慎,步履维艰。
依赖于操作系统的实现导致协议僵化
TCP 是由操作系统在内核西方栈层面实现的,应用程序只能使用,不能直接修改。虽然应用程序的更新迭代非常快速和简单。但是 TCP 的迭代却非常缓慢,原因就是操作系统升级很麻烦。
现在移动终端更加流行,但是移动端部分用户的操作系统升级依然可能滞后数年时间。PC 端的系统升级滞后得更加严重,windows xp 现在还有大量用户在使用,尽管它已经存在快 20 年。
服务端系统不依赖用户升级,但是由于操作系统升级涉及到底层软件和运行库的更新,所以也比较保守和缓慢。
这也就意味着即使 TCP 有比较好的特性更新,也很难快速推广。比如 TCP Fast Open。它虽然 2013 年就被提出了,但是 Windows 很多系统版本依然不支持它。
队头阻塞
队头阻塞主要是 TCP 协议的可靠性机制引入的。TCP 使用序列号来标识数据的顺序,数据必须按照顺序处理,如果前面的数据丢失,后面的数据就算到达了也不会通知应用层来处理。
另外 TLS 协议层面也有一个队头阻塞,因为 TLS 协议都是按照 record 来处理数据的,如果一个 record 中丢失了数据,也会导致整个 record 无法正确处理。
概括来讲,TCP 和 TLS1.2 之前的协议存在着结构性的问题,如果继续在现有的 TCP、TLS 协议之上实现一个全新的应用层协议,依赖于操作系统、中间设备还有用户的支持。部署成本非常高,阻力非常大。
所以 QUIC 协议选择了 UDP,因为 UDP 本身没有连接的概念,不需要三次握手,优化了连接建立的握手延迟,同时在应用程序层面实现了 TCP 的可靠性,TLS 的安全性和 HTTP2 的并发性,只需要用户端和服务端的应用程序支持 QUIC 协议,完全避开了操作系统和中间设备的限制。
建立时延低
0RTT 建连可以说是 QUIC 相比 HTTP2 最大的性能优势。那什么是 0RTT 建连呢?这里面有两层含义。
- 传输层 0RTT 就能建立连接。
- 加密层 0RTT 就能建立加密连接。
比如上图左边是 HTTPS 的一次完全握手的建连过程,需要 3 个 RTT。就算是 Session Resumption,也需要至少 2 个 RTT。
而 QUIC 呢?由于建立在 UDP 的基础上,同时又实现了 0RTT 的安全握手,所以在大部分情况下,只需要 0 个 RTT 就能实现数据发送,在实现前向加密 [15] 的基础上,并且 0RTT 的成功率相比 TLS 的 Sesison Ticket要高很多。
TCP 的拥塞控制实际上包含了四个算法:慢启动,拥塞避免,快速重传,快速恢复。
QUIC 协议当前默认使用了 TCP 协议的 Cubic 拥塞控制算法,同时也支持 CubicBytes, Reno, RenoBytes, BBR, PCC 等拥塞控制算法。
从拥塞算法本身来看,QUIC 只是按照 TCP 协议重新实现了一遍,那么 QUIC 协议到底改进在哪些方面呢?主要有如下几点:
可插拔
什么叫可插拔呢?就是能够非常灵活地生效,变更和停止。体现在如下方面:
- 应用程序层面就能实现不同的拥塞控制算法,不需要操作系统,不需要内核支持。这是一个飞跃,因为传统的 TCP 拥塞控制,必须要端到端的网络协议栈支持,才能实现控制效果。而内核和操作系统的部署成本非常高,升级周期很长,这在产品快速迭代,网络爆炸式增长的今天,显然有点满足不了需求。
- 即使是单个应用程序的不同连接也能支持配置不同的拥塞控制。就算是一台服务器,接入的用户网络环境也千差万别,结合大数据及人工智能处理,我们能为各个用户提供不同的但又更加精准更加有效的拥塞控制。比如 BBR 适合,Cubic 适合。
- 应用程序不需要停机和升级就能实现拥塞控制的变更,我们在服务端只需要修改一下配置,reload 一下,完全不需要停止服务就能实现拥塞控制的切换。
STGW 在配置层面进行了优化,我们可以针对不同业务,不同网络制式,甚至不同的 RTT,使用不同的拥塞控制算法。
单调递增的 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 按照顺序组织起来,交给应用程序处理。
不允许 Reneging
Reneging 是接收方丢弃已经接收并且上报给 SACK 选项的内容 。TCP 协议不鼓励这种行为,但是协议层面允许这样的行为。主要是考虑到服务器资源有限,比如 Buffer 溢出,内存不够等情况。
Reneging 对数据重传会产生很大的干扰。因为 Sack 都已经表明接收到了,但是接收端事实上丢弃了该数据。
QUIC 在协议层面禁止 Reneging,一个 Packet 只要被 Ack,就认为它一定被正确接收,减少了这种干扰。
更多的 Ack 块
TCP 的 Sack 选项能够告诉发送方已经接收到的连续 Segment 的范围,方便发送方进行选择性重传。
由于 TCP 头部最大只有 60 个字节,标准头部占用了 20 字节,所以 Tcp Option 最大长度只有 40 字节,再加上 Tcp Timestamp option 占用了 10 个字节,所以留给 Sack 选项的只有 30 个字节。
每一个 Sack Block 的长度是 8 个,加上 Sack Option 头部 2 个字节,也就意味着 Tcp Sack Option 最大只能提供 3 个 Block。
但是 Quic Ack Frame 可以同时提供 256 个 Ack Block,在丢包率比较高的网络下,更多的 Sack Block 可以提升网络的恢复速度,减少重传量。
Ack Delay 时间
Tcp 的 Timestamp 选项存在一个问题 [25],它只是回显了发送方的时间戳,但是没有计算接收端接收到 segment 到发送 Ack 该 segment 的时间。这个时间可以简称为 Ack Delay。
这样就会导致 RTT 计算误差。如下图:
基于 stream 和 connecton 级别的流量控制
QUIC 的流量控制类似 HTTP2,即在 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可用窗口+streamN可用窗口
同样地,STGW 也在连接和 Stream 级别设置了不同的窗口数。
最重要的是,我们可以在内存不足或者上游处理性能出现问题时,通过流量控制来限制传输速率,保障服务可用性。
没有队头阻塞的多路复用
QUIC 的多路复用和 HTTP2 类似。在一条 QUIC 连接上可以并发发送多个 HTTP 请求 (stream)。但是 QUIC 的多路复用相比 HTTP2 有一个很大的优势。
QUIC 一个连接上的多个 stream 之间没有依赖。这样假如 stream2 丢了一个 udp packet,也只会影响 stream2 的处理。不会影响 stream2 之前及之后的 stream 的处理。
这也就在很大程度上缓解甚至消除了队头阻塞的影响。
多路复用是 HTTP2 最强大的特性,能够将多条请求在一条 TCP 连接上同时发出去。但也恶化了 TCP 的一个问题,队头阻塞,如下图示:
HTTP2 在一个 TCP 连接上同时发送 4 个 Stream。其中 Stream1 已经正确到达,并被应用层读取。但是 Stream2 的第三个 tcp segment 丢失了,TCP 为了保证数据的可靠性,需要发送端重传第 3 个 segment 才能通知应用层读取接下去的数据,虽然这个时候 Stream3 和 Stream4 的全部数据已经到达了接收端,但都被阻塞住了。
不仅如此,由于 HTTP2 强制使用 TLS,还存在一个 TLS 协议层面的队头阻塞。
Record 是 TLS 协议处理的最小单位,最大不能超过 16K,一些服务器比如 Nginx 默认的大小就是 16K。由于一个 record 必须经过数据一致性校验才能进行加解密,所以一个 16K 的 record,就算丢了一个字节,也会导致已经接收到的 15.99K 数据无法处理,因为它不完整。
那 QUIC 多路复用为什么能避免上述问题呢?
- QUIC 最基本的传输单元是 Packet,不会超过 MTU 的大小,整个加密和认证过程都是基于 Packet 的,不会跨越多个 Packet。这样就能避免 TLS 协议存在的队头阻塞。
- Stream 之间相互独立,比如 Stream2 丢了一个 Pakcet,不会影响 Stream3 和 Stream4。不存在 TCP 队头阻塞。
当然,并不是所有的 QUIC 数据都不会受到队头阻塞的影响,比如 QUIC 当前也是使用 Hpack 压缩算法,由于算法的限制,丢失一个头部数据时,可能遇到队头阻塞。
总体来说,QUIC 在传输大量数据时,比如视频,受到队头阻塞的影响很小。
加密认证的报文
TCP 协议头部没有经过任何加密和认证,所以在传输过程中很容易被中间网络设备篡改,注入和窃听。比如修改序列号、滑动窗口。这些行为有可能是出于性能优化,也有可能是主动攻击。
但是 QUIC 的 packet 可以说是武装到了牙齿。除了个别报文比如 PUBLIC_RESET 和 CHLO,所有报文头部都是经过认证的,报文 Body 都是经过加密的。
这样只要对 QUIC 报文任何修改,接收端都能够及时发现,有效地降低了安全风险。
如下图所示,红色部分是 Stream Frame 的报文头部,有认证。绿色部分是报文内容,全部经过加密。
连接迁移
一条 TCP 连接 [17] 是由四元组标识的(源 IP,源端口,目的 IP,目的端口)。什么叫连接迁移呢?就是当其中任何一个元素发生变化时,这条连接依然维持着,能够保持业务逻辑不中断。当然这里面主要关注的是客户端的变化,因为客户端不可控并且网络环境经常发生变化,而服务端的 IP 和端口一般都是固定的。
比如大家使用手机在 WIFI 和 4G 移动网络切换时,客户端的 IP 肯定会发生变化,需要重新建立和服务端的 TCP 连接。
又比如大家使用公共 NAT 出口时,有些连接竞争时需要重新绑定端口,导致客户端的端口发生变化,同样需要重新建立 TCP 连接。
针对 TCP 的连接变化,MPTCP[5] 其实已经有了解决方案,但是由于 MPTCP 需要操作系统及网络协议栈支持,部署阻力非常大,目前并不适用。
所以从 TCP 连接的角度来讲,这个问题是无解的。
那 QUIC 是如何做到连接迁移呢?很简单,任何一条 QUIC 连接不再以 IP 及端口四元组标识,而是以一个 64 位的随机数作为 ID 来标识,这样就算 IP 或者端口发生变化时,只要 ID 不变,这条连接依然维持着,上层业务逻辑感知不到变化,不会中断,也就不需要重连。
由于这个 ID 是客户端随机产生的,并且长度有 64 位,所以冲突概率非常低。
参考资料
本文部分知识内容引用网络。
1.科普:QUIC协议原理分析 https://zhuanlan.zhihu.com/p/32553477