引言
2022年6月,HTTP/3正式被标准化为RFC 9114,其将是HTTP超文本传输协议的第三个主要版本。HTTP协议作为一个简单的请求-响应协议,各个主要版本主要是在于优化传输效率及安全性方面进行优化改进。
目前来说,仍存在大量服务器使用HTTP/1.1协议,其特点是,每个请求会独立建立一个TCP连接,而浏览器通常都会对单个域名的连接数进行限制,当连接数量过多,后续的请求就会排队等待。实际中,某些网站会采用多域名的方式去规避上述问题,但每一个请求的TCP建连开销仍然存在。
HTTP/2 采用多路复用技术,解决上述问题,其在一个TCP连接上同时传输多个请求和相应消息,如此排队等待的问题就迎刃而解了,同时也减少了TCP连接过程。除了多路复用提高请求的并发能力之外,还支持服务端主动推送以及头部压缩。其优点很多,但仍存在固有问题,和HTTP/1.1一样,其传输层基于TCP协议,由于TCP协议的丢包重传机制,如果一个请求发生丢包,浏览器会不断重传该数据包,进而阻塞后续请求,这也就是TCP的队头阻塞问题。
HTTP/3采用QUIC协议代替TCP协议提供可靠性传输,QUIC协议是基于UDP协议的,并采用了流的概念,不同的请求通过不同的有序字节流传输,即同一连接下互不影响的逻辑信道。同时具有流ID和流偏移2个字段,使得数据包可以乱序传输,而正确组装。当出现丢包时,仅重传出现丢包的流即可,并不会阻塞其他请求的传输,也就没有队头阻塞的问题。
QUIC
QUIC 源于Quick UDP Internet Connections,而如今是一个独立协议名称。QUIC 是一种基于UDP的安全、可靠、多路复用的传输协议。QUIC 是一种面向连接的协议,它在客户端和服务器之间创建有状态的交互。
QUIC的握手过程包含了加密和传输参数的协商,其集成了TLS握手。握手过程会尽快地完成数据交换,在有前置通信或配置的情况下,可以做到立即发送数据(0-RTT)。QUIC通信是基于数据包的,大多数数据包中含有1个或者更多的帧,这些帧携带控制信息或用户数据,最后利用UDP在网络中传输。上层应用在使用QUIC连接中的有序字节流交换数据,其可以创建两种流:双向流,允许两端均发送数据;单向流,仅允许单个端发送数据。QUIC 连接并不严格绑定到单个网络路径。 连接迁移使用连接标识符(不同于传统的五元组,使用64位随机数作为标识)完成连接转移到新的网络路径。 只有客户端才能在目前版本的 QUIC 中迁移。 还允许在网络拓扑或地址映射发生变化后继续连接,例如可能由 NAT 重新绑定引起的变化。
QUIC = TLS + TCP+HTTP/2 其整合了TCP的可靠性,TLS的安全性以及HTTP/2多路复用的并发性。
数据格式
Packet
long packet header
Long Header Packet {
Header Form (1) = 1,
Fixed Bit (1) = 1,
Long Packet Type (2),
Type-Specific Bits (4),
Version (32),
Destination Connection ID Length (8),
Destination Connection ID (0..160),
Source Connection ID Length (8),
Source Connection ID (0..160),
Type-Specific Payload (..),
}
long packet header 用于在 1-RTT 密钥建立之前发送的数据包。当1-RTT 密钥建立后,发送方会切换回short packet header。long packet header格式被用于特殊的数据包,例如版本协商数据包,其个字段含义如下:
Header Form: 对于long packet header第一个字节的最高位必须设置为1。
Fixed Bit:只有版本协商数据包该位会置为0,但目前未被使用,该位为0的数据包目前不会被处理,设置为1
Long Packet Type: 第一个字节的接下来2位表示数据包的类型,0为Initial Packet,1为0-RTT,2为Handshake Packet,3为Retry Packet,详细内容见最下方RFC文档
Type-Specific Bits: 第一个字节的低四位语义有数据包类型决定。
Version: QUIC 版本是跟在第一个字节之后的 32 位字段。 此字段指示正在使用的 QUIC 版本,并确定如何解释其余协议字段。
Destination Connection ID Length: 版本后面的字节包含其后面的目标连接 ID 字段的字节长度。8位无符号整数
Destination Connection ID: 连接标识ID
Source Connection ID Length: 源连接 ID 字段的字节长度。 8 位无符号整数。
Source Connection ID:源连接ID
Type-Specific Payload: 数据包的其余部分(如果有)是特定于类型的。
short packet header
Packet {
Header Form (1) ,
Fixed Bit (1) ,
Spin Bit (1),
Reserved Bits (2),
Key Phase (1),
Packet Number Length (2),
Destination Connection ID (0..160),
Packet Number (8..32),
Packet Payload (8..),
}
1-RTT 数据包使用short packet header。 它在版本协商和 1-RTT 密钥协商后使用。
Header Form: 对于short packet header,第一个字节的最高位设置为0。
Fixed Bit: 只有版本协商数据包该位会置为0,但目前未被使用,该位为0的数据包目前不会被处理,设置为1。
Spin Bit:第一个字节 的第三个最高有效位表示延迟自旋位。
Reserved Bits: 下两位被保留。
Key Phase: 下一位表示密钥阶段,它允许数据包的接收者识别用于保护数据包的数据包保护密钥。
Packet Number Length: 最低有效两位包含数据包编号字段的长度,编码为无符号两位整数,比数据包编号字段的长度(以字节为单位)小 1。 即 Packet Number 字段的长度是该字段的值加一。
Destination Connection ID: 连接标识ID
Packet Number:字段的长度为 1 到 4 个字节。 数据包编号使用Header protection进行加密。
Packet Payload:1-RTT 数据包总是包含一个 1-RTT 加密的有效载荷。
Frame
Packet (负载部分)包含一个或多个Frame。Frame的类型由很多,如PADDING Frames、PING Frames、ACK Frames 、RESET_STREAM Frames、STOP_SENDING Frames、CRYPTO Frames、STREAM Frames、CONNECTION_CLOSE Frames、HANDSHAKE_DONE Frames等等。
Frame {
Type (i) ,
[Type-Specific Payload],
}
Type: 表示帧的类型。
QUIC Stream数据流是QUIC主要数据传输的载体,STREAM Frames是其必要组成单元,以此为例简单介绍。
STREAM Frame {
Type (i) = 0x08..0x0f,
Stream ID (i),
[Offset (i)],
[Length (i)],
Stream Data (..),
}
Stream ID: 一个可变长度整数,表示流的流 ID。原版协议支出是一个62位的整数,一般认为1-4个字节长度
Offset: 一个可变长度整数,指定此 STREAM 帧中的数据在流中的字节偏移量。一般认为0-8个字节长度
Length: 一个可变长度整数,指定此 STREAM 帧中流数据字段的长度。一般认为0-2个字节长度
Stream Data: 被传递的指定流中的数据。
更详细的说明:https://zhuanlan.zhihu.com/p/405387352
握手
QUIC实现了快速握手,并把握手过程分为两种情况,分别是1-RTT和0-RTT。
1-RTT建连
若客户端没有服务器相关配置信息,只能进行1-RTT建连。
- 客户端主动向服务器发起连接,发送Inchoate CHLO 请求服务器配置参数。
- 服务器收到CHLO后,回复REJ(rejection)消息,其中含有服务器配置参数。
- 客户端向服务器发送complete CHLO消息,携带其生成的公开数,此时客户端已经可以根据服务端配置信息和自身选择的随机数,计算出初始密钥。
- 服务器收到complete CHLO消息,利用客户端的公开数计算出初始密钥,并向客户端回复用初始密钥加密的的SHLO消息,其含有一个服务器生成的临时随机数,用于生成会话密钥。
- 客户端收到SHLO消息,并用初始密钥解密,进而生成会话密钥。
- 客户端和服务器之前,利用会话密钥进行加密通信。
QUIC集成了TLS1.3,在协商完初始密钥后即会同时发送业务数据,并在更新会话密钥后切换到使用会话密钥,所有仅有1-RTT时间的延时。
0-RTT
如上所述,如果客户端缓存有服务器配置信息,可以直接发送complete CHLO消息,并发送加密数据。服务器收到消息后,会用初始密钥解密数据,并产生随机数用于生成会话密钥,并回复携带随机数的SHLO消息,客户端利用该随机数生成会话密钥,客户端和服务器之前,利用会话密钥进行加密通信。
在初始密钥后,再协商出一个最终的会话密钥,其目的是为了获取前向安全特性,服务端的后面生成的这份公私钥是临时生成的,不会保存下来,也就杜绝了密钥泄漏导致会话数据被恶意收集后的被解密的风险。
QUIC性能特点
建连更快
QUIC建连时间大约0~1 RTT,在两方面做了优化,一是传输层使用了UDP,相比于TCP减少了1RTT的三次握手的建连时间。二是加密协议采用了TLS 协议的最新版本TLS 1.3,相对之前的TLS 1.1-1.2,TLS1.3允许客户端无需等待TLS握手完成就开始发送应用程序数据,可以支持0-RTT和 1-RTT。
对于QUIC协议,客户端第一次建连的握手协商需1-RTT,而已建连的客户端重新建连可以使用之前协商好的缓存信息来恢复TLS连接,仅需0-RTT时间。因此QUIC建连时间大部分0-RTT、极少部分1-RTT,相比HTTPS的3-RTT的建连,具有极大的优势。
避免队头阻塞的多路复用
QUIC也支持多路复用,相比HTTP/2,QUIC的流与流之间完全隔离的,互相没有时序依赖。如果某个流出现丢包,仅重传丢包的流即可,不会阻塞其他流数据的传输和应用层处理,所以其并不会造成队头阻塞。
连接迁移
任何一条 QUIC 连接不再五元组作为标识,而是以一个 64 位的随机数作为 ID 来标识,这样就算 IP 或者端口发生变化时,只要 ID 不变,这条连接依然维持着,上层业务逻辑感知不到变化,不会中断,也就不需要重连。由于这个 ID 是客户端随机产生的,并且长度有 64 位,所以冲突概率非常低。
可选的拥塞控制算法
Cubic、BBR、Reno等拥塞控制算法,也可以根据具体的场景定制私有算法。
两级流量控制
QUIC同样采用了滑动窗口机制,在Connection和Stream两个级别分别进行流量控制。 用公式表示就是: connection可用窗口 = stream1可用窗口 + stream2可用窗口 + streamN可用窗口
前向纠错(FEC)
QUIC支持前向纠错,通过增加冗余数据,使数据包具有自动纠错的能力,可以减少重传次数,提升传输效率。
QUIC发展的一些问题
摘选自https://mp.weixin.qq.com/s/Ogfj0QlLF17sD60L3pwYXw
集成难度
无论是在客户端还是服务端,QUIC协议的集成并非一件易事。如果当前使用的网络不支持QUIC,意味着我们需要修改应用程序来适配网络库的调整。这往往并不是应用迭代的出发点。
下面让我们来看看客户端和服务端在集成QUIC协议时需要考虑的问题:
客户端
- 应用适配成本和收益之间的权衡。
- 过渡期可能还需要新旧网络库都存在,方便降级容错,增加应用体积。
- 不同的QUIC库的接口并不统一,不像Socket统一接口具备移植性。
服务端
- 网络事件模型需要适配QUIC协议栈做调整,同时还要考虑和TCP的兼容。
- 后端架构面临调整,4层Load Balancer是否支持QUIC,HTTP/3的QUIC流量如何换成HTTP/1转给Backend Service。
- 需要考虑到多Region、多节点之间的QUIC连接复用和连接迁移。
- 服务端QUIC流量的能耗比,如何做到和TCP一样的能耗性能。
QUIC协议栈性能
对比已经发展了三十多年的TCP协议,新兴的QUIC协议在协议栈实现的工程上还有很多优化的事情要做,根据Google 2017年公开的数据,当时QUIC同等流量的CPU消耗是TCP的2倍之多。QUIC协议栈的性能痛点主要有:
- UDP数据包收发性能
- 数据装包、拆包处理性能
- 数据包解析性能
- ACK处理性能
- 加、解密性能
- 其他流程处理的性能:数据包Pacing、CC算法、内存使用等
UDP数据包性能
UDP数据包在内核接收发送上一直没有得到和TCP数据包一样的优化待遇,UDP收发占据了QUIC协议栈比较大的消耗。
- 原始的sendto/write的UDP Socket接口性能很弱。
- 批量收发数据sendmsg/sendmmsg提升不够明显。
- 内核GRO/GSO可以提升性能,但还不够。
- 内核XDP或者DPDK可大幅提升性能,但程序改造量极大。
- 硬件NIC Offload UDP收发能终极优化,同样程序改造量极大。
UDP被运营商QoS限制
国内运营商确实会对UDP流量做些QoS的限制,但是能够被QoS限制的比例微乎其微,可以完全忽略。运营商这么做主要基于两个原因:
-
UDP流量五元组在NAT状态上连接老化的时间控制不够精确,时间设置太长会破坏低频通信的UDP连接,太短会导致UDP连接消耗比较多的设备性能。一般会选择一个折中的经验值,这就会伤害一些特定场景的UDP流量了。
-
运营商设备上包分类的优先级队列对于UDP五元组的管理比较困难,因为UDP的五元组会频繁变动,只能眼睁睁看着UDP流量挤占各级队列,却没办法实施精确地控制。一般来说,运营商会在特定时间段或者特定负载情况下对UDP流量做全局限制。
QUIC Datagram——QUIC的非可靠数据报文传输扩展
QUIC v1(RFC9000)提供了一个安全、可靠、多路复用的传输方案,QUIC Stream数据流是QUIC主要数据传输的载体,可靠传输是其需要保证的基本功能。
但是有些应用场景并不需要严格的可靠传输,例如实时音视频、在线游戏等。现在这些场景有些是直接基于自定义UDP或者WebRTC实现的。自定义UDP实现传输层功能的话比较麻烦,工程量大,有时候为了安全考虑还需要加上DTLS支持,而WebRTC的建立数据传输通道流程也比较繁琐,WebRTC主要场景是为了P2P设计的,长远来看并不适合客户端服务端的场景。
因此,QUIC支持不可靠数据传输能够让QUIC协议的应用范围更广,适合更多的应用场景,而且QUIC之上的不可靠数据传输能够很好地复用QUIC的安全特性。
QUIC Datagram特点
- 同一QUIC连接里面可以同时包含可靠Stream传输和不可靠数据传输,它们可以共享一次握手信息,分别可以作用于TLS和DTLS,可降低握手延迟。
- QUIC在握手上比DTLS更加精准,它对每个握手数据包加上了超时重传定时器,能够快速感知握手包的丢失和恢复。
- QUIC Datagram的不可靠传输也可以被ACK,这是一个选项,如果有ACK,可以让应用程序感知到QUIC Datagram的成功接收。
- QUIC Datagram也适用于QUIC的CC。
这些特性能够让实时音视频流、在线游戏和实时网络服务等应用的传输得到极大的效率提升。
参考文献
QUIC RFC9000 https://quicwg.org/base-drafts/rfc9000.html
QUIC Datagram: https://datatracker.ietf.org/doc/html/draft-ietf-quic-datagram-06
QUIC协议的发展 https://mp.weixin.qq.com/s/Ogfj0QlLF17sD60L3pwYXw
QUIC 是如何解决TCP 性能瓶颈的 https://mp.weixin.qq.com/s/PZ1IbXe396b6gzmWYpFk-Q