HTTP 3.0之QUIC优势和TCP弊端

1 HTTP 3.0

1.1 简介

1.1.1 引言

HTTP/1.1HTTP/2HTTP 协议一直都是使用 TCP 作为传输协议。
然而,就在最新的 HTTP/3HTTP 就直接把 TCP 抛弃了,向孤立无援的 UDP 伸出了援手,基于 UDP 协议的基础上,在应用层实现了一个可靠的传输协议 —— QUIC
在这里插入图片描述

很多同学可能就好奇了,HTTP 都用 TCP 都用了几十年了,而且 TCP 已经是那么完善的可靠传输协议了,又有超时重传、按序接收、流量控制、拥塞控制这些特性,怎么突然就把 TCP 抛弃了?到底是 TCP 哪里做的不够好?

1.1.2 QUIC 协议概览

QUIC (Quick UDP Internet Connections, 快速UDP网络连接)是基于UDP的协议, 利用了UDP的速度和效率, 同时整合TCP, TLSHTTP/2的优点并加以优化. 用一张图可以清晰的表示他们之间的关系.
在这里插入图片描述
QUIC是用来替代TCP, SSL/TLS的传输层协议, 在传输层之上还有应用层
我们熟知的应用层协议有HTTP, FTP, IMAP等, 这些协议理论上都可以运行在QUIC上, 其中运行在QUIC之上的协议被称为HTTP/3, 这就是HTTP over QUICHTTP/3的含义

1.2 队头阻塞/多路复用问题

HTTP/1.1HTTP/2都存在队头阻塞的问题(Head Of Line blocking)

TCP是个面向连接的协议, 即发送请求后需要收到ACK消息, 以确认对象已接受数据. 如果每次请求都要在收到上次请求的ACK消息后再请求, 那么效率无疑很低. 后来HTTP/1.1提出了Pipeline技术, 允许一个TCP连接同时发送多个请求. 这样就提升了传输效率.

在这样的背景下, 队头阻塞发生了. 比如, 一个TCP连接同时传输10个请求, 其中1,2,3个请求给客户端接收, 但是第四个请求丢失, 那么后面第5-10个请求都被阻塞. 需要等第四个请求处理完毕后才能被处理. 这样就浪费了带宽资源.

因此, HTTP一般又允许每个主机建立6个TCP连接, 这样可以更加充分的利用带宽资源, 但每个连接中队头阻塞的问题还是存在的.

TCP 队头阻塞的问题要从两个角度看,一个是发送窗口的队头阻塞,另外一个是接收窗口的队头阻塞

1.2.1 发送窗口的队头阻塞

TCP 发送出去的数据,都是需要按序确认的,只有在数据都被按顺序确认完后,发送窗口才会往前滑动。举个例子,比如下图的发送方把发送窗口内的数据全部都发出去了,可用窗口的大小就为 0 了,表明可用窗口耗尽,在没收到 ACK 确认之前是无法继续发送数据了。
在这里插入图片描述
接着,当发送方收到对第 32~36 字节的 ACK 确认应答后,则滑动窗口往左边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来第 52~56 字节又变成了可用窗口,那么后续也就可以发送 52~56 这 5 个字节的数据了。
在这里插入图片描述
但是如果某个数据报文丢失或者其对应的 ACK 报文在网络中丢失,会导致发送方无法移动发送窗口,这时就无法再发送新的数据,只能超时重传这个数据报文,直到收到这个重传报文的 ACK,发送窗口才会移动,继续后面的发送行为。

举个例子,比如下图,客户端是发送方,服务器是接收方
在这里插入图片描述
客户端发送了第 5~9 字节的数据,但是第 5 字节的 ACK 确认报文在网络中丢失了,那么即使客户端收到第 6~9 字节的 ACK 确认报文,发送窗口也不会往前移动。

此时的第 5 字节相当于队头,因为没有收到队头ACK 确认报文,导致发送窗口无法往前移动,此时发送方就无法继续发送后面的数据,相当于按下了发送行为的暂停键,这就是发送窗口的队头阻塞问题

1.2.2 接收窗口的队头阻塞

接收方收到的数据范围必须在接收窗口范围内,如果收到超过接收窗口范围的数据,就会丢弃该数据,比如下图接收窗口的范围是 32 ~ 51 字节,如果收到第 52 字节以上数据都会被丢弃
在这里插入图片描述
接收窗口什么时候才能滑动?当接收窗口收到有序数据时,接收窗口才能往前滑动,然后那些已经接收并且被确认的有序数据就可以被应用层读取。

但是,当接收窗口收到的数据不是有序的,比如收到第 33~40 字节的数据,由于第 32 字节数据没有收到, 接收窗口无法向前滑动,那么即使先收到第 33~40 字节的数据,这些数据也无法被应用层读取的。只有当发送方重传了第 32 字节数据并且被接收方收到后,接收窗口才会往前滑动,然后应用层才能从内核读取第 32~40 字节的数据

至此发送窗口和接收窗口的队头阻塞问题都说完了,这两个问题的原因都是因为 TCP 必须按序处理数据,也就是 TCP 层为了保证数据的有序性,只有在处理完有序的数据后,滑动窗口才能往前滑动,否则就停留。

停留发送窗口会使得发送方无法继续发送数据。
停留接收窗口会使得应用层无法读取新的数据。
其实也不能怪 TCP 协议,它本来设计目的就是为了保证数据的有序性

1.2.3 HTTP/2 的队头阻塞

HTTP/2 多路复用解决了上述的队头阻塞问题。在HTTP/2中, 每个请求都被拆分为多个Frame通过一条TCP连接同时被传输, 这样即使一个请求被阻塞, 也不会影响其他的请求.
但是, HTTP/2虽然可以解决请求这一粒度下的阻塞, 但HTTP/2的基础TCP协议本身却也存在队头阻塞的问题. HTTP/2的每个请求都会被拆分成多个Frame, 不同请求的Frame组合成Stream, StreamTCP上的逻辑传输单元, 这样HTTP/2就达到了一条连接同时发送多个请求的目标, 其中Stram1已经正确送达, Stram2中的第三个Frame丢失, TCP处理数据是有严格的前后顺序, 先发送的Frame要先被处理, 这样就会要求发送方重新发送第三个Frame, Steam3和Steam4虽然已到达但却不能被处理, 那么这时整条链路都会被阻塞

HTTP/2 通过抽象出 Stream 的概念,实现了 HTTP 并发传输,一个 Stream 就代表 HTTP/1.1 里的请求和响应
在这里插入图片描述
HTTP/2 连接上,不同 Stream是可以乱序发送的(因此可以并发不同的 Stream ),因为每个帧的头部会携带 Stream ID 信息,所以接收端可以通过 Stream ID 有序组装成 HTTP 消息,而同一 Stream 内部的帧必须是严格有序的。

但是 HTTP/2 多个 Stream 请求都是在一条 TCP 连接上传输,这意味着多个 Stream 共用同一个 TCP 滑动窗口,那么当发生数据丢失,滑动窗口是无法往前移动的,此时就会阻塞住所有的 HTTP 请求,这属于 TCP 层队头阻塞。
在这里插入图片描述

不仅如此,由于HTTP/2必须使用HTTPS, 而HTTPS使用TLS协议也存在队头阻塞问题. TLS基于Record组织数据, 将一对数据放在一起加密, 加密完成后又拆分成多个TCP包传输. 一般每个Record 16K, 包含12个TCP包, 这样如果12个TCP包中有任何一个包丢失, 那么整个Record都无法解密

队头阻塞会导致HTTP/2在更容易丢包的弱网络环境下比HTTP/1.1更慢.

1.2.4 没有队头阻塞的 QUIC

QUIC解决队头阻塞的问题主要有两点:

  • QUIC的传输单位是Packet, 加密单元也是Packet, 整个加密, 传输, 解密都基于Packet, 这就能避免TLS的阻塞问题.
  • QUIC基于UDP, UDP的数据包在接收端没有处理顺序, 即使中间丢失一个包, 也不会阻塞整条连接. 其他的资源会被正常处理.

QUIC 也借鉴 HTTP/2 里的 Stream 的概念,在一条 QUIC 连接上可以并发发送多个 HTTP 请求 (Stream)。但是 QUIC 给每一个 Stream 都分配了一个独立的滑动窗口,这样使得一个连接上的多个 Stream 之间没有依赖关系,都是相互独立的,各自控制的滑动窗口。

假如 Stream2 丢了一个 UDP 包,也只会影响 Stream2 的处理,不会影响其他 Stream,与 HTTP/2 不同,HTTP/2 只要某个流中的数据包丢失了,其他流也会因此受影响。
在这里插入图片描述

1.3 TCP 建立连接的延迟

对于 HTTP/1HTTP/2 协议,TCPTLS 是分层的,分别属于内核实现的传输层、openssl 库实现的表示层,因此它们难以合并在一起,需要分批次来握手,先 TCP 握手(1RTT),再 TLS 握手(2RTT),所以需要 3RTT 的延迟才能传输数据,就算 Session 会话服务,也需要至少 2 个 RTT,这在一定程序上增加了数据传输的延迟。

RTT: round-trip time, 仅包括请求访问来回的时间

TCP 三次握手和 TLS 握手延迟,如图:
在这里插入图片描述

HTTP/3 在传输数据前虽然需要 QUIC 协议握手,这个握手过程只需要 1 RTT,握手的目的是为确认双方的连接 ID,连接迁移就是基于连接 ID 实现的。
但是 HTTP/3QUIC 协议并不是与 TLS 分层,因为 QUIC 也是应用层实现的协议,所以可以将 QUICTLS 协议握手的过程合并在一起,QUIC 内部包含了 TLS,它在自己的帧会携带 TLS 里的 记录,再加上 QUIC 使用的是 TLS1.3,因此仅需 1 个 RTT 就可以 同时 完成建立连接与密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果。

如下图右边部分,HTTP/3 当会话恢复时,有效负载数据与第一个数据包一起发送,可以做到 0-RTT(下图的右下角):
在这里插入图片描述

1.4 HTTP/3 连接过程

HTTP/3首次连接只需要1RTT, 后面的链接只需要0RTT, 意味着客户端发送给服务端的第一个包就带有请求数据, 其主要连接过程如下:

  • 首次连接,客户端发送Inchoate Client Hello, 用于请求连接;
  • 服务端生成g, p, a, 根据g, p, a算出A, 然后将g, p, A放到Server Config中在发送Rejection消息给客户端.
  • 客户端接收到g,p,A后, 自己再生成b, 根据g,p,a算出B, 根据A,p,b算出初始密钥K,B和K算好后, 客户端会用K加密HTTP数据, 连同B一起发送给服务端.
  • 服务端接收到B后, 根据a,p,B生成与客户端同样的密钥, 再用这密钥解密收到的HTTP数据. 为了进一步的安全(前向安全性), 服务端会更新自己的随机数a和公钥, 在生成新的密钥S, 然后把公钥通过Server Hello发送给客户端. 连同Server Hello消息, 还有HTTP返回数据.
    在这里插入图片描述
    这里使用DH密钥交换算法, DH算法的核心就是服务端生成a,g,p 3个随机数, a自己持有, g和p要传输给客户端, 而客户端会生成b这 1个随机数, 通过DH算法客户端和服务端可以算出同样的密钥。在这过程中a和b并不参与网络传输, 安全性大大提升。因为p和g是大数, 所以即使在网络传输中p, g, A, B都被劫持, 靠现在的计算力算力也无法破解

1.5 连接迁移影响

基于 TCP 传输协议的 HTTP 协议,由于是通过 四元组(源IP源端口目的 IP目的端口)确定一条 TCP 连接
在这里插入图片描述
那么当移动设备的网络从 4G 切换到 WIFI 时,意味着 IP 地址变化了,那么就必须要断开连接,然后重新建立 TCP 连接。
在这里插入图片描述

而建立连接的过程包含 TCP 三次握手和 TLS 四次握手的时延,以及 TCP 慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。
QUIC 协议没有用四元组的方式来绑定连接,而是通过 连接ID来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 IDTLS 密钥等),就可以 无缝 地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。
在这里插入图片描述

1.6 拥塞控制影响

拥塞控制的目的是避免过多的数据一下子涌入网络, 导致网络超出最大负荷. QUIC的拥塞控制与TCP类似, 并在此基础上做了改进. 先来看看TCP的拥塞控制

  • 慢启动: 发送方向接收方发送一个单位的数据, 收到确认后发送2个单位, 然后是4个, 8个依次指数增长, 这个过程中不断试探网络的拥塞程度.
  • 避免拥塞: 指数增长到某个限制之后, 指数增长变为线性增长
  • 快速重传: 发送方每一次发送都会设置一个超时计时器, 超时后认为丢失, 需要重发
  • 快速恢复: 在上面快速重传的基础上, 发送方重新发送数据时, 也会启动一个超时定时器, 如果收到确认消息则进入拥塞避免阶段, 如果仍然超时, 则回到慢启动阶段.

QUIC重新实现了TCP协议中的Cubic算法进行拥塞控制, 下面是QUIC改进的拥塞控制的特性:

  • 热插拔
    TCP中如果要修改拥塞控制策略, 需要在系统层面进行那个操作, QUIC修改拥塞控制策略只需要在应用层操作, 并且QUIC会根据不同的网络环境, 用户来动态选择拥塞控制算法.
  • 前向纠错 FEC
    QUIC使用前向纠错(FEC, Forword Error Correction)技术增加协议的容错性. 一段数据被切分为10个包后, 一次对每个包进行异或运算, 运算结果会作为FEC包与数据包一起被传输, 如果传输过程中有一个数据包丢失, 那么就可以根据剩余9个包以及FEC包推算出丢失的那个包的数据, 这样就大大增加了协议的容错性.
    这是符合现阶段网络传输技术的一种方案, 现阶段带宽已经不是网络传输的瓶颈, 往返时间才是, 所以新的网络传输协议可以适当增加数据冗余, 减少重传操作.
  • 单调递增的Packer Number
    TCP为了保证可靠性, 使用Sequence NumberACK来确认消息是否有序到达, 但这样的设计存在缺陷.
    超时发生后客户端发起重传, 后来接受到了ACK确认消息, 但因为原始请求和重传请求接受到的ACK消息一样, 所以客户端就不知道这个ACK对应的是原始请求还是重传请求. 这就会造成歧义.
    RTT: Round Trip Time, 往返事件
    RTO: Retransmission Timeout, 超时重传时间
    如果客户端认为是重传的ACK, 但实际上是右图的情形, 会导致RTT偏小, 反之会导致RTT偏大.
    在这里插入图片描述
    QUIC 解决了上面的的歧义问题, 与Sequence Number不同, Packet Number严格单调递增, 如果Packet N丢失了, 那么重传时Packet的标识就不会是N, 而是比N大的数字, 比如N+M, 这样发送方接收到确认消息时, 就能方便的知道ACK对应的原始请求还是重传请求.
  • ACK Delay
    TCP计算RTT时没有考虑接收方接受到数据发发送方确认消息之间的延迟, 如下图所示, 这段延迟即ACK DelayQUIC考虑了这段延迟, 使得RTT的计算更加准确.
    在这里插入图片描述
  • 更多的ACK块
    一般来说, 接收方收到发送方的消息后都应该发送一个ACK恢复, 表示收到了数据. 但每收到一个数据就返回一个ACK恢复实在太麻烦, 所以一般不会立即回复, 而是接受到多个数据后再回复, TCP SACK最多提供3个ACK block. 但在有些场景下, 比如下载, 只需要服务器返回数据就好, 但按照TCP的设计, 每收到三个数据包就要返回一个ACK, 而QUIC最多可以捎带256个ACK block, 在丢包率比较严重的网络下, 更多的ACK可以减少重传量, 提升网络效率

1.7 流量控制影响

TCP会对每个TCP连接进行流量控制, 流量控制的意思是让发送方不要发送太快, 要让接收方来得及接受, 不然会导致数据溢出而丢失, TCP的流量控制主要通过滑动窗口来实现的. 可以看到, 拥塞控制主要是控制发送方的发送策略, 但没有考虑接收方的接收能力, 流量控制则是对接收方接收能力的一种补充

QUIC只需要建立一条连接, 在这条连接上同时传输多条Stream, 好比有一条道路, 两边都分别有一个仓库, 道路中有很多车辆运送物资. QUIC的流量控制有两个级别: 连接级别(Connection Level)和Stream 级别(Stream Level).

对于单条的Stream的流量控制: Stream还没传输数据时, 接收窗口(flow control recevice window)就是最大接收窗口, 随着接收方接收到数据后, 接收窗口不断缩小. 在接收到的数据中, 有的数据已被处理, 而有的数据还没来得及处理. 如下图, 蓝色块表示已处理数据, 黄色块表示违背处理数据, 这部分数据的到来, 使得Stream的接收窗口缩小.
在这里插入图片描述

随着数据不断被处理, 接收方就有能力处理更多数据. 当满足 (flow control receivce offset - consumed bytes) < (max receive window/2) 时, 接收方会发送WINDOW_UPDATE frame 告诉发送方你可以再多发送数据, 这时候flow control receive offset就会偏移, 接收窗口增大, 发送方可以发送更多数据到接收方.
在这里插入图片描述
Stream级别对防止接收端接收过多数据作用有限, 更需要借助Connection级别的流量控制. 理解了Stream流量那么也很好理解Connection的流控.
Stream中,接收窗口=最大接受窗口 - 已接收数据
而对于Connection来说:接收窗口 = Stream1 接收窗口 + Stream2 接收窗口 + ... + StreamN 接收窗口

1.8 升级 TCP 的工作很困难

TCP 协议是诞生在 1973 年,至今 TCP 协议依然还在实现更多的新特性。
但是 TCP 协议是在内核中实现的,应用程序只能使用不能修改,如果要想升级 TCP 协议,那么只能升级内核。
而升级内核这个工作是很麻烦的事情,麻烦的事情不是说升级内核这个操作很麻烦,而是由于内核升级涉及到底层软件和运行库的更新,我们的服务程序就需要回归测试是否兼容新的内核版本,所以服务器的内核升级也比较保守和缓慢。
很多 TCP 协议的新特性,都是需要客户端和服务端同时支持才能生效的,比如 TCP Fast Open 这个特性,虽然在2013 年就被提出了,但是 Windows 很多系统版本依然不支持它,这是因为 PC 端的系统升级滞后很严重,Windows Xp 现在还有大量用户在使用,尽管它已经存在快 20 年。

所以,即使 TCP 有比较好的特性更新,也很难快速推广,用户往往要几年或者十年才能体验到。

相反,QUIC 是处于应用层的,所以如果升级 QUIC 协议的话,其实就是像升级软件一样轻松。而且,QUIC 可以针对不同的应用设置不同的拥塞控制算法,这样灵活性就很高了,这是 TCP 做不到的,因为 TCP 更改拥塞控制算法是对系统中所有应用都生效,无法根据不同应用设定不同的拥塞控制策略

1.9 总结

HTTP/3 抛弃 TCP 后,基于 UDP 实现的可靠传输 QUIC 协议,带来这四点好处:

  • 降低连接耗时:在客户端有缓存的情况下实现0-RTT建立连接
  • 更灵活的拥塞控制:在用户态可以为每个请求配置不同的拥塞控制策略
  • 无队头阻塞的多路复用:每个请求流独立拥有滑动窗口,互不影响
  • 连接迁移:网络切换不会中断数据传输

不过, HTTP/3 也面临了一些挑战,QUIC 基于 UDP 协议在用户空间实现的可靠传输协议,如果一些网络设备无法识别出 QUIC 协议,那么在这些网络设备的眼里它就是一个 UDP 协议。

而几乎所有的电信运营商都会 歧视 UDP 数据包,原因也很容易理解,毕竟历史上几次臭名昭著的 DDoS(分布式拒绝服务攻击) 攻击都是基于 UDP 的。国内运营商即使没有封禁 UDP,也是对 UDP 进行严格限流的。

自 2013 年 QUIC 被正式公开以来,到 2023 年已经发展了差不多 10 年,目前网上已经有了不少热门开源的项目,除去带头大哥 Google 在完成了对自身搜索引擎的支持,还同时拉上了 Gmail 、YouTube 等站点。但对于国内的绝大部分站点来说,大部分还是 HTTP/2 协议,HTTP/3 之路,似乎还停留在东土大唐

posted @ 2023-03-30 09:48  上善若泪  阅读(239)  评论(0编辑  收藏  举报