简介
QUIC
(Quick UDP Internet Connection)是谷歌制定的一种互联网传输层协议,它基于UDP
传输层协议,同时兼具TCP
、TLS
、HTTP/2
等协议的可靠性与安全性,可以有效减少连接与传输延迟,更好地应对当前传输层与应用层的挑战
QUIC
协议是一系列协议的集合,主要包括:
- 传输协议(Transport)
- 丢包检测与拥塞控制(Recovery)
- 安全传输协议(TLS)
- HTTP3协议
- HTTP头部压缩协议(QPACK)
- 负载均衡协议(Load Balance)
QUIC
是基于UDP
协议,实现了类似TCP
的可靠传输,并在此基础上,结合HTTP3/QPACK
,更好地服务互联网上海量的HTTP Request/Response
需求
QUIC/HTTP3
的特点:
- 有序传输:用
stream
的概念,确保数据有序。不同的stream
或者packet
,不保证有序到达。 - 报文压缩,提高荷载比率:比如
QUIC
引入了variable-length
integer
encoding
。又比如引入QPACK
进行头部压缩 - 可靠传输:支持丢包检测和重传
- 安全传输:
TLS 1.3
安全协议
分层的协议
QUIC
是在UDP
的基础上,构建类似TCP
的可靠传输协议。HTTP3
则在QUIC
基础上完成HTTP
事务
- UDP层: 在
UDP
层传输的是UDP
报文,此处关注的是UDP
报文荷载内容是什么,以及如何高效发送UDP
报文 - Connection层:
Connection
通过CID
来确认唯一连接,Connection
对packet
进行可靠传输和安全传输 - Stream层:
Stream
在相应的Connection
中,通过StreamID
进行唯一流确认,Stream
对Stream Frame
进行传输管理 - HTTP3层:
HTTP3
建立在QUIC Stream
的基础上,相对于HTTP1.1
和HTTP2.0
,HTTP3
提供更有效率的HTTP
事务传输。HTTP3
中通过QPACK
协议进行头部压缩
UDP层
UDP荷载大小
荷载大小受限于3个对象:
QUIC协议规定
路径MTU
终端接受能力
QUIC
不能运行在不支持1200
字节的单个UDP
传输网络路径上 QUIC
有规定initial
包大小不得小于1200
,如果数据本身不足1200
(比如initial ack),那么需要用padding
方式至少填充到1200
字节
QUIC
不希望出现IP层分片现象本要求意味着UDP
交给IP
层的数据不会大于1个MTU
,假设MTU
为1500,ipv4场景下,UDP
的荷载上限为1472
字节(1500-20-8),ipv6下,UDP
荷载上限为1452
(1500-40-8)。QUIC
建议使用PMTUD
以及DPLPMTUD
进行MTU
探测。在实战中,我们建议设置IPv6的MTU
为1280
,大于这个值,某些网络会存在丢包现象
终端能接受 transport paraments
的max_udp_payload_size(0x03)的是终端接受单个UDP
包大小的能力,发送端应当遵从这一约定
UDP荷载内容
UDP
荷载内容即为QUIC
协议中的Packet
。协议规定,如果不超过荷载大小的限制,那么多个Packet
可以组成一个UDP
报文发出去。在QUIC
实现中,如果每个UDP
报文只包含一个QUIC Packet
,会更容易出现乱序问题
高效发UDP包
和TCP
不同,QUIC
需要在应用层就完成UDP
数据组装,且每个UDP
报文不大于1个mtu,如果不加以优化,比如每个包直接用sendto/sendmsg
发送,势必会造成大量的系统调用,影响吞吐
- 通过
sendmmsg
接口进行优化,sendmmsg
可以将用户态的多个UDP
QUIC
包通过一次系统调用发到内核态。内核态对于每个UDP
QUIC
包独立作为UDP
包发出去 - 1解决了系统调用次数问题,开启
GSO
可以进步一分包延迟到发给网卡驱动前一刻,可以进一步提高吞吐,降低CPU消耗 - 2的基础上,现在主流网卡已经支持硬件
GSO
offload方案,可以进一步提高吞吐,降低cpu消耗
Connection层
在
Connection
这一层面,其实是以Packet
为单位进行管理的。一个Packet
到来,终端需要解析出目标ConnectionID
(DCID)字段,并将该Packet
交给找到对应的QUIC
Connection
。一个Packet
是由Header
加Payload
两部分组成
Connection ID
不同于TCP
的4元组唯一确认一条连接的方式,QUIC
定义了一个和网络路由无关的ConnectionID
来确认唯一连接的。这带来一个好处,可以在四元组发生变化时(比如nat rebinding或者终端网络切换wifi->4G),依然保持连接。当然,虽然连接状态依然保持,但由于路径发生变化,拥塞控制也需要能够及时调整
Packet头部
分为两种类型,Long Header, Short Header
。其中Long Header
有分为 initial
, 0rtt
, handshake
, retry
四种类型
QUIC
规定Packet Number
始终为自增的,就算某个Packet
的内容为重传的Frame
数据,其Packet Number
也必须自增,这相对于TCP
来说,带来一个优点,能够更加精确的采集到路径的RTT
属性
Packet Number
是一个0~262 -1
的取值范围,QUIC
为了节约空间,在计算Packet Number
时,引入了unacked
的概念,通过截断(只保留有效bit位)的方式,只用了1-4个字节,即可以encode/decode出正确的Packet Number
Packet
头在安全传输中是被保护对象,这也意味着在没有SSL
信息的情况下,无法使用wireshake对Packet
进行时序分析。中间网络设备也无法向TCP
那样获得Packet Number
进行乱序重组
Packet荷载
在对Packet
进行解密,且去除掉Packet Header
后,Packet
的荷载里就都是Frame
了(至少包括1个)
如果Packet
的荷载里,不包括ACK
, PADDING
, CONNECTION\_CLOSE
这种三种类型的帧,那么这个Packet
则被定义为ack-eliciting
,意味着对端必须对这种Packet
生成相应的ACK
通知发送方,以确保数据没有丢失
Packet
的荷载里Frames
的类型在多达30种类型,每种类型都有自己的应用场景,如ACK Frame
用于可靠传输(Recovery),Crypto
用于安全传输(TLS握手),Stream Frame
用于业务数据传递,MAX\_DATA/DATA\_BLOCKED
用于流控,PING Frame
可以用于MTU
探测等
安全传输
QUIC
的安全传输依赖TLS1.3
,而boringssl
是众多QUIC
实现的依赖库。协议对Packet
的头部以及荷载均进行了保护(包括Packet Number
)。TLS1.3
0RTT
的能力,在提供数据保护的同时,能在第一时间(服务端收到第一个请求报文时)就将Response Header
发给客户端。大大降低了HTTP
业务中的首包时间。为了支持0RTT
,客户端需要保存PSK
信息,以及部分Transport Parament
信息
安全传输也经常会涉及到性能问题,在目前主流的服务端,AESG
由于CPU提供了硬件加速,所以性能表现最好。CHACHA20
则需要更多的CPU资源。在短视频业务上,出于对首帧的要求,通常直接使用明文传输
Transport Paramenter
(TP)协商是在安全传输的握手阶段完成,除了协议规定的TP
外,用户也可以扩展私有TP
内容,这一特性带来了很大的便利,比如:客户端可以利用TP
告知服务端进行明文传输
可靠传输
QUIC
协议是需要像TCP
能够进行可靠传输,所以QUIC
单独有一个RFC
描述了丢包检测和拥塞控制的话题
- 丢包检测
协议利用两种方式来判断丢包是否发生:
一种是基于ACK
的检测,通过time threshold
和packet threshold
根据已经到达的Packet
,推断在此包之前发出去的包是否丢失。
第二种,在失去了参考包的情况下,那么只能通过PTO
的方式来推断包是否丢失。一般来说,大量被触发的应该是ACK
的检测方式。如果PTO
被大量触发,会影响发包效率
- 拥塞控制
QUIC
针对TCP
协议中的一些缺陷,专门做了优化。比如始终递增的Packet Number
,丰富的ack range,host delay计算等。
同时TCP
的拥塞控制需要内核态实现,而QUIC
在用户态实现,这大大降低了研究高效率的可靠传输协议的门槛。Recovery
协议中,描述了newRen
o的实现方式。
在Google Chrome
中,实现了cubic
, bbr
, bbrv2
,而mvfst
项目则更为丰富,包括了ccp
, copa
协议
Stream层
Stream
是一个抽象的概念,它表达了一个有序传输的字节流,而这些字节其实就是由Stream Frame
排在一起构成。在一个QUIC Connection
上,可以同时传输多条流
Stream头部
在QUIC
协议里,Stream
分为单向流或双向流,又分为客户端发起或服务端发起。Stream
的不同类型定义在HTTP3
中得到了充分的利用
Stream荷载
Stream
的荷载即为一系列Stream Frame
,通过Stream Frame
头部的Stream ID
来确认单个流。
在TCP
里,如果一个Segment
传递丢失,那么后续Segment
乱序到达,也不会被应用层使用,只到丢失的Segment
重传成功为止,因此TCP
实现的HTTP2
的多路复用能力受到制约。
在QUIC
协议中,有序的概念仅维护在单个Stream
中,Stream
之间和Packet
都不要求有序,假设某个Packet
丢失,只会影响包含在这个包里的Stream
,其他Stream
仍然可以从后续乱序到达的Packet
中提取到自己所需要的数据交给应用层
HTTP3层
在引入HTTP3
后,Stream
的单向流类型被扩展成:控制流,Push流和其他保留类型。其中HTTP3
的setting则是在控制流中传输,而HTTP
数据传输是在客户端发起的双向流中,所以读者会发现,HTTP
数据传输的Stream ID
都是模4等于0的
引入QPACK
后,单向流被进一步扩展了两个类型,encoder流,decoder流,QPACK
中动态表的更新则依赖这两个流
QPACK
的作用是头部压缩。类似HPACK
,QPACK
定义了静态表,动态表用于头部索引。静态表是针对常见的头部,协议预先定义的。
动态表则是在该QUIC Connection
服务HTTP
过程中,逐渐建立的。QPACK
所建立的Encoder/Decoder
流是伴随用于HTTP
事务的QUIC Connection
生命周期。
动态表不是HTTP3
能够运行的必须项,所以在某些QUIC
开源项目中,并没有实现复杂的动态表功能
在QPACK
的动态表业务中,数据流,编码流,解码流3种对象共同参与,编码流和解码流负责维护动态表变化,数据流则解析出头部的索引号,去动态表中查询,得到最终的头部定义
其它
Flow Control 流控
QUIC
协议引入了Flow Control
的概念,用于表达接收端的接受能力。流控分两级,Connection
级别,和Stream
级别。
发送端发送的数据偏移量不能超过流控的限制,如果达到限制,那么发送端应该通过 DATA\_BLOCKED/STREAM\_DATA\_BLOCKED
来通知接收端。
如果为了传输性能,接收端应该尽量保持限制足够大,比如达到MAX\_DATA
的一半时,就及时更新MAX\_DATA
传给发送端。如果接收端不希望太快接受数据,也可以利用流控对发送端进行约束
QUIC版本
不同版本可能是无法互通的,比如不同版本安全传输的salt变量规定不一样。所以IQUIC
引入了版本协商的功能,用于不同的客户端和服务端协商出可以互通的版本
要求一个服务能够同时服务GQUIC
的不同版本,又能服务IQUIC
的不同版本。这就要求服务在收取到Packet
后,需要对Packet
作出判断,然后进行逻辑分流