重新整理:极客重生 文章目录 一、QUIC 如何解决TCP的队头阻塞问题? 二、QUIC 如何优化TCP 的连接管理机制? 三、QUIC 如何改进TCP 的拥塞控制机制? 1.1 TCP 为何会有队头阻塞问题 HTTP/2 相比HTTP/1.1 设计出的一些优秀的改进方案,大幅提高了HTTP 的网络利用效率。HTTP/2 在应用协议层通过多路复用同一个TCP连接解决了队头阻塞问题,但这是以下层协议比如TCP 协议不出现任何数据包阻塞为前提的。TCP 在实际运行中,特别是遇到网络环境不好时,数据包超时确认或丢失是常有的事,假如某个数据包丢失需要重传时会发生什么呢?
TCP 采用正面确认和超时重传机制来保证数据包的可靠交付。比如主机A 向 主机B 发送数据包,主机B 收到该数据包后会向主机A 返回确认应答报文,表示自己确实收到了该数据包,主机A 收到确认应答报文后才确定上一个数据包已经发送成功,开始发送下一个数据包。如果超过一定时间(根据每次测量的往返时间RTT估算出的动态阈值)未收到确认应答,则主机A 判断上一个数据包丢失了,重新发送上一个数据包,这就相当于阻塞了下一个数据包的发送。
逐个发送数据包,等待确认应答到来后再发送下一个数据包,效率太低了,TCP 采用滑动窗口机制来提高数据传输效率。窗口大小就是指无需等待确认应答而可以继续发送数据的最大值,这个机制实现了使用大量的缓冲区,通过对多个数据包同时进行确认应答的功能。当可发送数据的窗口消耗殆尽时,就需要等待收到连续的确认应答后,当前窗口才会向前滑动,为发送下一批数据包腾出窗口。假设某个数据包超时未收到确认应答,当前窗口就会阻塞在原地,重新发送该数据包,在收到该重发数据包的确认应答前,就不会有新增的可发送数据包了。也就是说,因为某个数据包丢失,当前窗口阻塞在原地,同样阻塞了后续所有数据包的发送。
TCP 因为超时确认或丢包引起的滑动窗口阻塞问题,是不是有点像HTTP/1.1 管道化机制中出现的队头阻塞问题?HTTP/2 在应用协议层通过多路复用解决了队头阻塞问题,但TCP 在传输层依然存在队头阻塞问题,这是TCP 协议的一个主要性能瓶颈。该怎么解决TCP 的队头阻塞问题呢? 1.2 QUIC 如何解决队头阻塞问题? TCP 队头阻塞的主要原因是数据包超时确认或丢失阻塞了当前窗口向右滑动,我们最容易想到的解决队头阻塞的方案是不让超时确认或丢失的数据包将当前窗口阻塞在原地。QUIC (Quick UDP Internet Connections)也正是采用上述方案来解决TCP 队头阻塞问题的。 TCP 为了保证可靠性,使用了基于字节序号的 Sequence Number 及 Ack 来确认消息的有序到达。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 编号并不一致,我们怎么确定这两个数据包的内容一样呢? 还记得前篇博文:HTTP/2 是如何解决HTTP/1.1 性能瓶颈的?使用Stream ID 来标识当前数据流属于哪个资源请求,这同时也是数据包多路复用传输到接收端后能正常组装的依据。重传的数据包Packet N+M 和丢失的数据包Packet N 单靠Stream ID 的比对一致仍然不能判断两个数据包内容一致,还需要再新增一个字段Stream Offset,标识当前数据包在当前Stream ID 中的字节偏移量。
上图中数据包Packet N 丢失了,后面重传该数据包的编号为Packet N+2,丢失的数据包和重传的数据包Stream ID 与 Offset 都一致,说明这两个数据包的内容一致。这些数据包传输到接收端后,接收端能根据Stream ID 与 Offset 字段信息正确组装成完整的资源。 QUIC 通过单向递增的Packet Number,配合Stream ID 与 Offset 字段信息,可以支持非连续确认应答Ack而不影响数据包的正确组装,摆脱了TCP 必须按顺序确认应答Ack 的限制(也即不能出现非连续的空位),解决了TCP 因某个数据包重传而阻塞后续所有待发送数据包的问题(也即队头阻塞问题)。 QUIC 可以支持非连续的数据包确认应答Ack,自然也就要求每个数据包的确认应答Ack 都能返回给发送端(TCP 中间丢失几个Ack 对数据包的确认应答影响不大),发送端收到该数据包的确认应答后才会释放该数据包所占用的缓存资源,已发送但未收到确认应答的数据包会保存在缓存链表中等待可能的重传。QUIC 对确认应答Ack 丢失的容忍度比较低,自然对Ack 的传输能力进行了增强,Quic Ack Frame 可以同时提供 256 个 Ack Block,在丢包率比较高的网络下,更多的 Ack Block 可以提高Ack 送达的成功率,减少重传量。 1.3 QUIC 没有队头阻塞的多路复用 QUIC 解决了TCP 的队头阻塞问题,同时继承了HTTP/2 的多路复用优点,因为Stream Offset 字段的引入,QUIC 中同一Stream ID 的数据帧也支持乱序传输,不再像HTTP/2 要求的同一Stream ID 的数据帧必须有序传输那么严格。
从上面QUIC 的数据包结构中可以看出,同一个Connection ID 可以同时传输多个Stream ID,由于QUIC 支持非连续的Packet Number 确认,某个Packet N 超时确认或丢失,不会影响其它未包含在该数据包中的Stream Frame 的正常传输。同一个Packet Number 可承载多个Stream Frame,若该数据包丢失,则其承载的Stream Frame 都需要重新传输。因为同一Stream ID 的数据帧乱序传输后也能正确组装,这些需要重传的Stream Frame 并不会影响其它待发送Stream Frame 的正常传输。 值得一提的是,TLS 协议加解密前需要对数据进行完整性校验,HTTP/2 中如果TCP 出现丢包,TLS 也会因接收到的数据不完整而无法对其进行处理,也即HTTP/2 中的TLS 协议层也存在队头阻塞问题,该问题如何解决呢?既然TLS 协议是因为接收数据不完整引起的阻塞,我们只需要让TLS 加密认证过程基于一个独立的Packet,不对多个Packet 同时进行加密认证,就能解决TLS 协议层出现的队头阻塞问题,某一个Packet 丢失只会影响封装该Packet 的Record,不会让其它Record 陷入阻塞等待的情况。 2.1 TCP连接的本质是什么? 你可能熟悉TCP 建立连接的三次握手和四次挥手过程,但你知道TCP 建立的连接本质上是什么吗?这里的连接跟我们熟悉的物理介质连接(比如电路连接)不同,主要是用来说明如何在物理介质上传输数据的。 为了更直观了解网络连接概念,我们拿面向连接的TCP 与无连接的UDP 做对比,网络传输层的两个主流协议,他们的主要区别是什么呢?UDP 每个分组的处理都独立于所有其他分组,TCP 每个分组的传输都有确认应答过程和可能的丢包重传过程,需要为每个分组数据进行状态信息记录和管理(比如未发送、已发送、未确认、已确认等状态)。 TCP 建立连接的三次握手过程都做了哪些工作呢?首先确认双方是否能正常收发数据,通信双方交换待发送数据的初始序列编号并作为有序确认应答的基点,通信双方根据预设的状态转换图完成各自的状态迁移过程,通信双方为分组数据的可靠传输和状态信息的记录管理分配控制块缓存资源等。下面给出TCP 连接建立、数据传输、连接释放三个阶段的报文交互过程和状态迁移图示(详见博文:TCP协议与Transmission Control Protocol):
从上图可以看出,TCP 连接主要是双方记录并同步维护的状态组成的。一般来说,建立连接是为了维护前后分组数据的承继关系,维护前后承继关系最常用的方法就是对其进行状态记录和管理。 TCP 的状态管理可以分为连接状态管理和分组数据状态管理两种,连接状态管理用于双方同步数据发送与接收状态,分组数据状态管理用于保证数据的可靠传输。涉及到状态管理一般都有状态转换图,TCP 连接管理的状态转换图上面已经给出了,HTTP/2 的Stream 实际上也记录并维护了每个Stream Frame 的状态信息,Stream 的状态转换图如下:
2.2 QUIC 如何减少TCP 建立连接的开销? TCP 建立连接需要三次握手过程,第三次握手报文发出后不需要等待应答回复就可以发送数据报文了,所以TCP 建立连接的开销为 1-RTT。既然TCP 连接主要是由双方记录并同步维护的状态组成的,我们能否借鉴TLS 快速恢复简短握手相比完整握手的优化方案呢? TLS 简短握手过程是将之前完整握手过程协商的信息记录下来,以Session Ticket 的形式传输给客户端,如果想恢复之前的会话连接,可以将Session Ticket 发送给服务器,就能通过简短的握手过程重建或者恢复之前的连接,通过复用之前的握手信息可以节省 1-RTT 的连接建立开销。 TCP 也提供了快速建立连接的方案 TFO (TCP Fast Open),原理跟TLS 类似,也是将首次建立连接的状态信息记录下来,以Cookie 的形式传输给客户端,如果想复用之前的连接,可以将Cookie 发送给服务器,如果服务器通过验证就能快速恢复之前的连接,TFO 技术可以通过复用之前的连接将连接建立开销缩短为 0-RTT。因为TCP 协议内置于操作系统中,操作系统的升级普及过程较慢,因此TFO 技术至今仍未普及(TFO 在2014年发布于RFC 7413)。
从上图可知,TCP 首次建立连接的开销为 1-RTT,快速复用/打开连接的开销为 0-RTT,这与TLS 1.3 协议首次完整握手与快速恢复简短握手的开销一致。 客户端发送的第一个SYN 握手包是可以携带数据的,但为了防止TCP 泛洪攻击,TCP 的实现者不允许将SYN 携带的数据包上传给应用层。HTTP 协议中TCP 与TLS 常常配合使用,这里TCP 的第一个SYN 握手包可以携带TLS 1.3 的握手包,这就可以将TCP + TLS 总的握手开销进一步降低。 首次建立连接时,TCP 和TLS 1.3 都只需要 1-RTT 就可以完成握手过程,由于TCP 第一个SYN 握手包可以携带TLS 的握手包,因此TCP + TLS 1.3 总的首次建立连接开销为 1-RTT。当要快速恢复之前的连接时,TFO 和TLS 1.3 都只需要 0-RTT 就可以完成握手过程,因此TCP + TLS 1.3 总的连接恢复开销为 0-RTT。 QUIC 可以理解为”TCP + TLS 1.3“(QUIC 是基于UDP的,可能使用的是DTLS 1.3),QUIC 自然也实现了首次建立连接的开销为 1-RTT,快速恢复先前连接的开销为 0-RTT 的效率。QUIC 作为HTTP/2 的改进版,建立连接的开销也有明显降低,下面给出HTTP/2 和QUIC 首次连接和会话恢复过程中,HTTP 请求首个资源的RTT 开销对比: HTTP/2 + TLS 1.2 首次连接HTTP/2 + TLS 1.2 会话恢复HTTP/2 + TLS 1.3 首次连接HTTP/2 + TLS 1.3 会话恢复HTTP/2 + TLS 1.3 会话恢复 + TFOQUIC 首次连接QUIC 会话恢复 DNS 解析 1-RTT 0-RTT 1-RTT 0-RTT 0-RTT 1-RTT 0-RTT TCP 握手 1-RTT 1-RTT 1-RTT 1-RTT 0-RTT (TCP Fast Open) TLS 握手 2-RTT 1-RTT 1-RTT 0-RTT 0-RTT QUIC 握手 1-RTT 0-RTT