HTTP 3.0之QUIC优势和TCP弊端
1 HTTP 3.0
1.1 简介
1.1.1 引言
从 HTTP/1.1
到 HTTP/2
,HTTP
协议一直都是使用 TCP
作为传输协议。
然而,就在最新的 HTTP/3
,HTTP
就直接把 TCP
抛弃了,向孤立无援的 UDP
伸出了援手,基于 UDP
协议的基础上,在应用层实现了一个可靠的传输协议 —— QUIC
。
很多同学可能就好奇了,HTTP
都用 TCP
都用了几十年了,而且 TCP
已经是那么完善的可靠传输协议了,又有超时重传、按序接收、流量控制、拥塞控制这些特性,怎么突然就把 TCP
抛弃了?到底是 TCP
哪里做的不够好?
1.1.2 QUIC 协议概览
QUIC
(Quick UDP Internet Connections
, 快速UDP网络连接)是基于UDP
的协议, 利用了UDP
的速度和效率, 同时整合TCP
, TLS
和HTTP/2
的优点并加以优化. 用一张图可以清晰的表示他们之间的关系.
QUIC
是用来替代TCP, SSL/TLS
的传输层协议, 在传输层之上还有应用层
我们熟知的应用层协议有HTTP, FTP, IMAP
等, 这些协议理论上都可以运行在QUIC
上, 其中运行在QUIC
之上的协议被称为HTTP/3
, 这就是HTTP over QUIC
即HTTP/3
的含义
1.2 队头阻塞/多路复用问题
HTTP/1.1
和HTTP/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
, Stream
是TCP
上的逻辑传输单元, 这样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/1
和 HTTP/2
协议,TCP
和 TLS
是分层的,分别属于内核实现的传输层、openssl
库实现的表示层,因此它们难以合并在一起,需要分批次来握手,先 TCP
握手(1RTT),再 TLS
握手(2RTT),所以需要 3RTT 的延迟才能传输数据,就算 Session
会话服务,也需要至少 2 个 RTT,这在一定程序上增加了数据传输的延迟。
RTT: round-trip time
, 仅包括请求访问来回的时间
TCP
三次握手和 TLS
握手延迟,如图:
HTTP/3
在传输数据前虽然需要 QUIC
协议握手,这个握手过程只需要 1 RTT
,握手的目的是为确认双方的连接 ID
,连接迁移就是基于连接 ID
实现的。
但是 HTTP/3
的 QUIC
协议并不是与 TLS
分层,因为 QUIC
也是应用层实现的协议,所以可以将 QUIC
和 TLS
协议握手的过程合并在一起,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
地址变化了,只要仍保有上下文信息(比如连接 ID
、TLS 密钥
等),就可以 无缝 地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。
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 Number
和ACK
来确认消息是否有序到达, 但这样的设计存在缺陷.
超时发生后客户端发起重传, 后来接受到了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 Delay
。QUIC
考虑了这段延迟, 使得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 之路,似乎还停留在东土大唐