一文读懂QUIC 协议:更快、更稳、更高效的网络通信

你是否也有这样的困扰:打开APP巨耗时、刷剧一直在缓冲、追热搜打不开页面、信号稍微差点就直接加载失败……

如果有一个协议能让你的上网速度,在不需要任何修改的情况下就能提升20%,特别是网络差的环境下能够提升30%以上;如果有一个协议可以让你在WiFi和蜂窝数据切换时,网络完全不断开、直播不卡顿、视频不缓冲;你愿意去了解一下它吗?它就是QUIC协议。本文将从QUIC的背景、原理、实践部署等方面来详细介绍。

一:网络协议栈  

1.1 什么叫网络协议?  

类似于我们生活中签署的合同一样,比如买卖合同是为了约束买卖双方的行为按照合同的要求履行,网络协议是为了约束网络通信过程中各方(客户端、服务端及中间设备)必须按照协议的规定进行通信,它制定了数据包的格式、数据交互的过程等等,网络中的所有设备都必须严格遵守才可以全网互联。

在网络协议栈中,是有分层的,每一层负责不同的事务。我们讨论最多的有三个:应用层、传输层、网络层。应用层主要是针对应用软件进行约束,比如你访问网站需要按照HTTP协议格式和要求进行,你发送电子邮件需要遵守SMTP等邮件协议的格式和要求;传输层主要负责数据包在网络中的传输问题,比如如何保证数据传输的时候的安全性和可靠性、数据包丢了怎么处理;网络层,也叫路由转发层,主要负责数据包从出发地到目的地,应该怎样选择路径才能更快的到达。合理的网络协议能够让用户上网更快!    

1.2 HTTP/3协议  

HTTP/3是第三个主要版本的HTTP协议。与其前任HTTP/1.1和HTTP/2不同,在HTTP/3中,弃用TCP协议,改为使用基于UDP协议的QUIC协议实现。所以,HTTP/3的核心在于QUIC协议。显然,HTTP/3属于应用层协议,而它使用的QUIC协议属于传输层协议。

1.3 我们需要HTTP/3协议吗  

很多人可能都会有这样一个疑问,为什么在 2015 年才标准化了 HTTP/2 ,这么快就需要 HTTP/3?

我们知道,HTTP/2通过引入“流”的概念,实现了多路复用。简单来说,假设你访问某个网站需要请求10个资源,你使用HTTP1.1协议只能串行地发请求,资源1请求成功之后才能发送资源2的请求,以此类推,这个过程是非常耗时的。如果想10个请求并发,不需要串行等待的话,在HTTP1.1中,应用就需要为一个域名同时建立10个TCP连接才行(一般浏览器不允许建立这么多),这无疑是对资源的极大的浪费。HTTP/2的多路复用解决了这一问题,能使多条请求并发。

但现实很残酷,为什么很多业务用了HTTP/2,反倒不如HTTP1.1呢?

第一:多流并发带来了请求优先级的问题,因为有的请求客户端(比如浏览器)希望它能尽快返回,有的请求可以晚点返回;又或者有的请求需要依赖别的请求的资源来展示。流的优先级表示了这个请求被处理的优先级,比如客户端请求的关键的CSS和JS资源是必须高优先级返回的,图片视频等资源可以晚一点响应。         
流的优先级的设置是一个难以平衡或者难以做到公平合理的事情,如果设置稍微不恰当,就会导致有些请求很慢,这在用户看来,就是用了HTTP/2之后,怎么有的请求变慢了。    

第二:HTTP/2解决了HTTP协议层面的队头阻塞,但是TCP的队头阻塞仍然没有解决,所有的流都在一条TCP连接上,如果万一序号小的某个包丢了,那么TCP为了保证到达的有序性,必须等这个包到达后才能滑动窗口,即使后面的序号大的包已经到达了也不能被应用程序读取。这就导致了在多条流并发的时候,某条流的某个包丢了,序号在该包后面的其他流的数据都不能被应用程序读取。这种情况下如果换做HTTP1.1,由于HTTP1.1是多条连接,某个连接上的请求丢包了,并不影响其他连接。所以在丢包比较严重的情况下,HTTP/2整体效果大概率不如HTTP1.1

事实上,我们并不是真的需要新的 HTTP 版本,而是需要对底层传输控制协议(TCP) 进行升级。

1.4 QUIC协议栈  

83a553b2452b201dcaf19711c42fa2b0.png

图0-QUIC协议栈    

QUIC协议实现在用户态,建立在内核态的UDP的基础之上,集成了TCP的可靠传输特性,集成了TLS1.3协议,保证了用户数据传输的安全。

二:QUIC协议的优秀特性  

2.1 建连快  

数据的发送和接收,要想保证安全和可靠,一定是需要连接的。TCP需要,QUIC也同样需要。连接到底是什么?连接是一个通道,是在一个客户端和一个服务端之间的唯一一条可信的通道,主要是为了安全考虑,建立了连接,也就是建立了可信通道,服务器对这个客户端“很放心”,对于服务器来说:你想跟我进行通信,得先让我认识一下你,我得先确认一下你是好人,是有资格跟我通信的。那么这个确认对方身份的过程,就是建立连接的过程。

传统基于TCP的HTTPS的建连过程为什么如此慢?它需要TCP和TLS两个建连过程。如图1所示(传统HTTPS请求流程图):    

89a760131cc8fc8c59ac153b14ae4f7d.png

图1-传统HTTPS请求流程图

对于一个小请求(用户数据量较小)而言,传输数据只需要1个RTT,但是光建连就花掉了3个RTT,这是非常不划算的,这里建连包括两个过程:TCP建连需要1个RTT,TLS建连需要2个RTT。RTT:Round Trip Time,数据包在网络上一个来回的时间。

为什么需要两个过程?可恶就可恶在这个地方,TCP和TLS没办法合并,因为TCP是在内核里完成的,TLS是在用户态。也许有人会说把干掉内核里的TCP,把TCP挪出来放到用户态,然后就可以和TLS一起处理了。首先,你干不掉内核里的TCP,TCP太古老了,全世界的服务器的TCP都固化在内核里了。所以,既然干不掉TCP,那我不用它了,我再自创一个传输层协议,放到用户态,然后再结合TLS,这样不就可以把两个建连过程合二为一了吗?是的,这就是QUIC。

2.1.1 QUIC的1-RTT建连    

如图2所示,是QUIC的连接建立过程:初次建连只需要1个RTT即可完成建连。后续再次建连就可以使用0-RTT特性

9954d6083d826e5883459bc3e59ad2b5.png

图2-QUIC建连过程图

QUIC的1-RTT建连:客户端与服务端初次建连(之前从未进行通信过),或者长时间没有通信过(0-RTT过期了),只能进行1-RTT建连。只有先进行一次完整的1-RTT建连,后续一段时间内的通信才可以进行0-RTT建连。

如图3所示:QUIC的1-RTT建连可以分成两个部分。QUIC连接信息部分和TLS1.3握手部分。    

a18becc451726ab73b235a97a2883e07.png

图3-QUIC建连抓包

QUIC连接:协商QUIC版本号、协商quic传输参数、生成连接ID、确定Packet Number等信息,类似于TCP的SYN报文;保证通信的两端确认过彼此,是对的人。

TLS1.3握手:标准协议,非对称加密,目的是为了协商出 对称密钥,然后后续传输的数据使用这个对称密钥进行加密和解密,保护数据不被窃取。

我们重点看QUIC的TLS1.3握手过程。    

1ef153d0354f4e10082213432ee8cca1.png

图4-QUIC的1-RTT握手流程

我们通过图4可以看到,整个握手过程需要 2次握手(第三次握手是带了数据的),所以整个握手过程只需要1-RTT(RTT是指数据包在网络上的一个来回)的时间。

1-RTT的握手主要包含两个过程:

1.客户端发送Client Hello给服务端;

2.服务端回复Server Hello给客户端;

我们通过下图中图5和图6来看Client Hello和Server Hello具体都做了啥:

第一次握手(Client Hello报文)    

d7d1f8fb369206502e71c7603cbf68d3.png

图5-Client Hello报文

首先,Client Hello在扩展字段里标明了支持的TLS版本(Supported Version:TLS1.3)。值得注意的是Version字段必须要是TLS1.2,这是因为TLS1.2已经在互联网上存在了10年。网络中大量的网络中间设备都十分老旧,这些网络设备会识别中间的TLS握手头部,所以TLS1.3的出现如果引入了未知的TLS Version 必然会存在大量的握手失败。    

ad71b4dff848faf568b7e87073f7ac46.png

图6-Client Hello报文

其次,ClientHello中包含了非常重要的key_share扩展:客户端在发送之前,会自己根据DHE算法生成一个公私钥对。发送Client Hello报文的时候会把这个公钥发过去,那么这个公钥就存在于key_share中,key_share还包含了客户端所选择的曲线X25519。总之,key_share是客户端提前生成好的公钥信息。

最后,Client Hello里还包括了:客户端支持的算法套、客户端所支持的椭圆曲线以及签名算法、psk的模式等等,一起发给服务端。    

ae7108d77ea8cdfbac14057805c093ce.png

图7-Client Hello报文

第二次握手:(Server Hello报文)    

f341d8b4b9023434c571be6ee9acd1a9.png

图8-Server Hello报文

服务端自己根据DHE算法也生成了一个公私钥对,同样的,Key_share扩展信息中也包含了 服务端的公钥信息。服务端通过ServerHello报文将这些信息发送给客户端。

至此为止,双方(客户端服务端)都拿到了对方的公钥信息,然后结合自己的私钥信息,生成pre-master key,在这里官方的叫法是(client_handshake_traffic_secret和server_handshake_traffic_secret),然后根据以下算法进行算出key和iv,使用key和iv对Server Hello之后所有的握手消息进行加密。

注意:在握手完成之后,服务端会发送一个New Session Ticket报文给客户端,这个包非常重要,这是0-RTT实现的基础。    

ab24186cfb2fb69b8b9b4bafc18467ba.png

图9-New Session Ticket报文

2.1.2 QUIC的0-RTT握手  

这个功能类似于TLS1.2的会话复用,或者说0-RTT是基于会话复用功能的。

befc00c4fd043f6e4894c26f8d49525d.png

图10- QUIC的0-RTT流程图

通过上面图10我们可以看到,client和server在建连时,仍然需要两次握手,仍然需要1个rtt,但是为什么我们说这是0-rtt呢,是因为client在发送第一个包client hello时,就带上了数据(HTTP 请求),从什么时候开始发送数据这个角度上来看,的确是0-RTT。

我们通过抓包来看0-RTT的过程:

e957dd8cd2da3967d91f778e7912cd6e.png

图11- QUIC的0-RTT抓包

所以真正在实现0-RTT的时候,请求的数据并不会跟Initial报文(内含Client Hello)一起发送,而是单独一个数据包(0-RTT包)进行发送,只不过是跟Initial包同时进行发送而已。

02a344709c5dc9c5ccda21bf2758b6b4.png

图12- QUIC的0-RTT包    

我们单独看Initial报文发现,除了pre_share_key、early-data标识等信息与1-RTT时不同,其他并无区别。

2.1.3 QUIC建连需要注意的问题  

第一,QUIC实现的时候,必须缓存收到的乱序加密帧,这个缓存至少要大于4096字节。当然可以选择缓存更多的数据,更大的缓存上限意味着可以交换更大的密钥或证书。终端的缓存区大小不必在整个连接生命周期内保持不变。这里记住:乱序帧一定要缓存下来。如果不缓存,会导致连接失败。如果终端的缓存区不够用了,则其可以通过暂时扩大缓存空间确保握手完成。如果终端不扩大其缓存,则其必须以错误码CRYPTO_BUFFER_EXCEEDED关闭连接。

第二,0-RTT存在前向安全问题,请慎用!

2.2连接迁移  

QUIC通过连接ID实现了连接迁移。

我们经常需要在WiFi和4G之间进行切换,比如我们在家里时使用WiFi,出门在路上,切换到4G或5G,到了商场,又连上了商场的WiFi,到了餐厅,又切换到了餐厅的WiFi,所以我们的日常生活中需要经常性的切换网络,那每一次的切换网络,都将导致我们的IP地址发生变化。

传统的TCP协议是以四元组(源IP地址、源端口号、目的ID地址、目的端口号)来标识一条连接,那么一旦四元组的任何一个元素发生了改变,这条连接就会断掉,那么这条连接中正在传输的数据就会断掉,切换到新的网络后可能需要重新去建立连接,然后重新发送数据。这将会导致用户的网络会“卡”一下。    

但是,QUIC不再以四元组作为唯一标识,QUIC使用连接ID来标识一条连接,无论你的网络如何切换,只要连接ID不变,那么这条连接就不会断,这就叫连接迁移!

c054c35dd7f2d3f067315f2ae311e79f.png

图13-QUIC连接迁移介绍

2.2.1连接ID  

每条连接拥有一组连接标识符,也就是连接ID,每个连接ID都能标识这条连接。连接ID是由一端独立选择的,每个端(客户端和服务端统称为端)选择连接ID供对端使用。也就是说,客户端生成的连接ID供服务端使用(服务端发送数据时使用客户端生成的连接ID作为目的连接ID),反过来一样的。

连接ID的主要功能是确保底层协议(UDP、IP及更底层的协议栈)发生地址变更(比如IP地址变了,或者端口号变了)时不会导致一个QUIC连接的数据包被传输到错误的QUIC终端(客户端和服务端统称为终端)上。

2.2.2 QUIC的连接迁移过程    

QUIC限制连接迁移为仅客户端可以发起,客户端负责发起所有迁移。如果客户端接收到了一个未知的服务器发来的数据包,那么客户端必须丢弃这些数据包。

如图14所示,连接迁移过程总共需要四个步骤。

1.连接迁移之前,客户端使用IP1和服务端进行通信;

2.客户端IP变成IP2,并且使用IP2发送非探测帧给服务端;

3.启动路径验证(双方都需要互相验证),通过PATH_CHANLLENGE帧和PATH_RESPONSE帧进行验证。

4.验证通过后,使用IP2进行通信。

097fb5f9ed3867035355210fc09df024.png

图14- 连接迁移流程图   

2.3 解决TCP队头阻塞问题  

在HTTP/2中引入了流的概念。目的是实现 多个请求在同一个连接上并发,从而提升网页加载的效率。

21c62f48c742a1a9b7d51bfbee82c3a7.png

图15-QUIC解决TCP队头阻塞问题

由图15来看,假设有两个请求同时发送,红色的是请求1,蓝色的是请求2,这两个请求在两条不同的流中进行传输。假设在传输过程中,请求1的某个数据包丢了,如果是TCP,即使请求2的所有数据包都收到了,但是也只能阻塞在内核缓冲区中,无法交给应用层。但是QUIC就不一样了,请求1的数据包丢了只会阻塞请求1,请求2不会受到阻塞。

有些人不禁发问,不是说HTTP2也有流的概念吗,为什么只有QUIC才能解决呢,这个根本原因就在于,HTTP2的传输层用的TCP,TCP的实现是在内核态的,而流是实现在用户态度,TCP是看不到“流”的,所以在TCP中,它不知道这个数据包是请求1还是请求2的,只会根据seq number来判断包的先后顺序。

2.4 更优的拥塞控制算法  

拥塞控制算法中最重要的一个参数是 RTT,RTT的准确性决定了拥塞控制算法的准确性;然而,TCP的RTT测量往往不准确,QUIC的RTT测量是准确的。    

d21fec7024209b73c7b608094db67cab.png

图16-TCP计算RTT

如图16所示:由于网络中经常出现丢包,需要重传,在TCP协议中,初始包和重传包的序号是一样的,拥塞控制算法进行计算RTT的时候,无法区别是初始包还是重传包,这将导致RTT的计算值要么偏大,要么偏小。

b2548762767d9c9ef501abac0904ffe5.png

图17-QUIC计算RTT    

如图17所示:QUIC通过Packet Number来标识包的序号,而且规定Packet Number只能单调递增,这也就解决了初始包和重传包的二义性。从而保证RTT的值是准确的。

另外,不同于TCP,QUIC的拥塞控制算法是可插拔的,由于其实现在用户态,服务可以根据不同的业务,甚至不同的连接灵活选择使用不同的拥塞控制算法。(Reno、New Reno、Cubic、BBR等算法都有自己适合的场景)

2.5 QUIC的两级流量控制  

很多人搞不清楚流量控制与拥塞控制的区别。二者有本质上的区别。

流量控制要解决的问题是:接收方控制发送方的数据发送的速度,就是我的接收能力就那么大点,你别发太快了,你发太快了我承受不住,会给你丢掉 你还得重新发。         
拥塞控制要解决的问题是:数据在网络的传输过程中,是否网络有拥塞,是否有丢包,是否有乱序等问题。如果中间传输的时候网络特别卡,数据包丢在中间了,发送方就需要重传,那么怎么判断是否拥塞了,重传要怎么重传法,按照什么算法进行发送数据才能尽可能避免数据包在中间路径丢掉,这是拥塞控制的核心。

所以,流量控制解决的是接收方的接收能力问题,一般采用滑动窗口算法;拥塞控制要解决的是中间传输的时候网络是否拥堵的问题,一般采用慢启动、拥塞避免、拥塞恢复、快速重传等算法。        

              

43106b174b7aa61426c07ac0951fb218.png

图18-QUIC流量控制

QUIC是双级流控,不仅有连接这一个级别的流控,还有流这个级别的流控。如下图所示,每个流都有自己的可用窗口,可用窗口的大小取决于最大窗口数减去发送出去的最大偏移数,跟中间已经发送出去的数据包,是否按顺序收到了对端的ACK 无关。

3.QUIC协议如何优化  

QUIC协议定义了很多优秀的功能,但是在实现的过程中,我们会遇到很多问题导致无法达到预期的性能,比如0-RTT率很低,连接迁移失败率很高等等。

3.1 QUIC的0-RTT成功率不高  

导致0-RTT成功率不高的原因一般有如下几个:

1.服务端一般都是集群,对于客户端来说,处理请求的服务端是不固定的,新的请求到来时,如果当前client没有请求过该服务器,则服务器上没有相关会话信息,会把该请求当做一个新的连接来处理,重新走1-RTT。    

针对此种情况,我们可以考虑集群中所有的服务器使用相同的ticket文件。

2.客户端IP不是固定的,在发生连接迁移时,服务端下发的token融合了客户端的IP,这个IP变化了的话,携带token服务端校验不过,0-RTT会失败。

针对这个问题,我们可以考虑采用如图19所示的方法,使用设备信息或者APP信息来生成token,唯一标识一个客户端。

13c588320e9653f2189b4fb724b249c4.png

图19- 使用设备信息提高0-RTT的成功率

3.Session Ticket过期时间默认是2天,超过2天后就会导致0-RTT失败,然后降级走1-RTT。可以考虑增长过期时间。

3.2 实现连接迁移并不容易  

连接迁移的实现,不可避开的两个问题:一个是四层负载均衡器对连接迁移的影响,一个是七层负载均衡器对连接迁移的影响。

四层负载均衡器的影响:LVS、DPVS等四层负载均衡工具基于四元组进行转发,当连接迁移发生时,四元组会发生变化,该组件就会把同一个请求的数据包发送到不同的后端服务器上,导致连接迁移失败;    

七层负载均衡器的影响(QUIC服务器多核的影响):由于多核的影响,一般服务器会有多个QUIC服务端进程,每个进程负载处理不同的连接。内核收到数据包后,会根据二元组(源IP、源port)选择已经存在的连接,并把数据包交给对应的socket。在连接迁移发生时,源地址发生改变,可能会让接下来的数据包去到不同的进程,影响socket数据的接收。

如何解决以上两个问题?DPVS要想支持QUIC的连接迁移,就不能再以四元组进行转发,需要以连接ID进行转发,需要建立 连接ID与对应的后端服务器的对应关系;

QUIC服务器也是一样的,内核就不能用四元组来进行查找socket,四元组查找不到时,就必须使用连接ID进行查找socket。但是内核代码又不能去修改(不可能去更新所有服务器的内核版本),那么我们可以使用eBPF的方法进行解决。如下图20所示:

776608134370a0c0b819bfc0d087c05c.png

图20-多核QUIC服务器解决连接迁移问题    

3.3 UDP被限速或禁闭  

业内统计数据全球有7%地区的运营商对UDP有限速或者禁闭,除了运营商还有很多企业、公共场合也会限制UDP流量甚至禁用UDP。这对使用UDP来承载QUIC协议的场景会带来致命的伤害。对此,我们可以采用多路竞速的方式使用TCP和QUIC同时建连。除了在建连进行竞速以外,还可以对网络QUIC和TCP的传输延时进行实时监控和对比,如果有链路对UDP进行了限速,可以动态从QUIC切换到TCP。

70b63c91594faefb1563eb9e30114e89.png

图21-QUIC和TCP协议竞速      

3.4 QUIC对CPU消耗大  

相对于TCP,为什么QUIC更消耗资源?

1.QUIC在用户态实现,需要更多的内核空间与用户空间的数据拷贝和上下文切换;

2.QUIC的ACK报文也是加密的,TCP是明文的。

3.内核知道TCP连接的状态,不用为每一个数据包去做诸如查找目的路由、防火墙规则等操作,只需要在tcp连接建立的时候做一次即可,然而QUIC不行;

总的来说,QUIC服务端消耗CPU的地方主要有三个:密码算法的开销;udp收发包的开销;协议栈的开销;    

针对这些,我们可以适当采取优化措施来:

1.使用Intel硬件加速卡卸载TLS握手

2.开启GSO功能。

3.数据在传输过程中,可以将一轮中所有的ACK解析后再同时进行处理,避免处理大量的ACK。

4.适当将QUIC的包长限制调高(比如从默认的1200调到1400个字节)

5.减少协议栈的内存拷贝

4.QUIC的性能  

从公开的数据来看,国内各个厂(腾讯、阿里、字节、华为、OPPO、网易等等)使用了QUIC协议后,都有很大的提升,比如网易上了QUIC后,响应速度提升45%,请求错误率降低50%;比如字节火山引擎使用QUIC后,建连耗时降低20%~30%;比如腾讯使用QUIC后,在腾讯会议、直播、游戏等场景耗时也降低30%;

总结  

QUIC协议的出现,为HTTP/3奠定了基础。这是近些年在web协议上最大的变革,也是最优秀的一次实践。面对新的协议,我们总是有着各种各样的担忧,诚然,QUIC协议在稳定性上在成熟度上,的确还不如TCP协议,但是经过近几年的发展,成熟度已经相当不错了,Nginx近期也发布了1.25.0版本,支持了QUIC协议。所以面对这样优秀的协议,我们希望更多的公司,更多的业务参与进来使用QUIC,推动QUIC更好的发展,推动用户上网速度更快!        

全方位剖析内核抢占机制

深入代码细节看f2fs在磁盘上的组织方式

抛开代码细节看f2fs文件系统组织方式

c3f0b099f02e506e0b0c6345ecb19be0.gif

长按关注内核工匠微信

Linux内核黑科技| 技术文章| 精选教程

  • 28
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

内核工匠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值