从HTTP2到QUIC
Posted Qzone前端
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从HTTP2到QUIC相关的知识,希望对你有一定的参考价值。
QCon是由InfoQ主办的全球顶级技术盛宴,在10月18日Qcon上海站的前端技术专场上,QQ空间的前端开发工程师黄佳琳以《从HTTP2到QUIC——QQ空间Web加速实践》为题,给大家分享了QQ空间在HTTP2和QUIC的实践过程中总结的一些经验,本文是根据现场分享内容所总结的笔录。
大家好,很荣幸今天能在这里跟大家做一个交流,今天我分享的主题是 《从HTTP2到QUIC——QQ空间Web加速实践》。我们知道,对于前端开发来说,页面的加载速度是我们穷尽一生都在努力提升的一项指标。得益于一些新技术、新协议的出现,我们又有了新的优化手段,也就是今天我要分享的两种新协议,HTTP2协议和QUIC协议。在开始之前 先简单做个自我介绍,我叫黄佳琳,目前在腾讯QQ空间任职,主要负责的是QQ空间前端的开发工作。
今天的分享内容我会分成三个部分,Web现状主要会介绍目前QQ空间web业务的一些基本情况,主要让大家对我们的应用场景有个大致的了解;协议演变部分主要介绍我们Web侧所使用的协议的演变过程,不同协议的原理和效果;最佳实践是我们使用了HTTP2和QUIC协议之后,现阶段所总结出的一些经验和坑。
QQ空间Web现状
相信很多业务都跟我们的情况差不多,我们的web业务现在主要有三种场景,不同的场景决定了我们采用不一样的协议。第一个场景是hybrid,也就是手机APP里的一些内嵌页面,第二个场景是在手机侧浏览器打开纯web页面,第三个场景是pc侧打开的纯web页面,对于这三种场景,目前我们都有应用HTTP2,QUIC主要在PC上应用,这三种情况下如果不支持HTTP2和QUIC,都会采用HTTP1.1来做一个降级策略。
协议演变
近三年来,QQ空间尝试了三种不同的协议,每一种协议都带来了速度上的提升。最开始的HTTP1.0我们就不说了嘛,当时我都还没上学,毫无痕迹地装个嫩。之前我们一直用HTTP1.1,基于HTTP1.1也做了很多的速度优化,大家熟知的一些雅虎军规之类的,但是还是一直在努力寻找下一个突破点。直到2014年,我们先开始尝试使用SPDY,我们知道SPDY其实就是HTTP2的前身,所以在启用SPDY初见成效之后,2015年我们很快也启用了HTTP2。来到了今年,我们开始使用QUIC。
大家对QUIC这个协议可能还都比较陌生,QUIC是一种可靠的,多路复用的网络协议,关于QUIC,最近报道的不少,但是实际应用的不多,QQ空间目前已经在生产环境开启了QUIC,先给大家看一下我们在QQ空间启用QUIC的一个截图:
这个截图是用chrome的开发者工具,打开我们QQ空间的黄钻页面,切到network这个tab,可以看到协议这里显示的是http/2+quic/39,39代表的是QUIC的版本,这表示我们已经启用了QUIC,但是你可能有个疑问,QUIC就显示QUIC,为什么要显示http/2+quic呢,两者是什么关系?我先用一张图简单展示一下QUIC和HTTP2的关系和差异:
我们可以看下,从协议层面上来看,TCP+TLS+HTTP2=UDP+QUIC+HTTP2’sAPI,在这里面,QUIC最重要的作用就是替代了TCP和TLS,而上层还是HTTP2的一些实现,这也是为什么我们刚才在chrome的协议栏看到展示的是http2+quic。QUIC所给我们提供的,是相当于HTTP/2的多路复用,相当于TLS的安全性,以及相当于TCP的连接语义、可靠性和拥塞控制。从上而下,我们先来了解一些HTTP2的特性,国内开启HTTP2的网站目前还不太多,其实HTTP2的特性是很重要的,可以看到不管是HTTP2协议本身还是QUIC,都包括了这一部分特性,了解HTTP2对web的加速和对后面了解QUIC都有必要。
HTTP2最重要的特性,我认为就是多路复用了,如何理解多路复用。我们可以先来看看以前是什么情况。左边是HTTP1.1的情况,假设你有三个请求,浏览器和服务器先建立连接,然后浏览器发起第一个请求,服务器返回数据,关闭这个连接。两端再次建立连接,发起第二个请求,返回数据,关闭连接,如此重复下去。这里最大的问题就是每次都需要重新建立连接。自然而然地出现了keep alive,keep alive跟之前最大的区别是,不需要每次重新建立连接,一定时间内,同一域名多次请求,只建立一次连接,其他请求可复用第一次建立的连接通道,以提高请求效率。
那这里的第二个问题来了,这里的请求都是串行的,必须等待前一个文件返回了才能继续进行下一个请求。
所以又出现了pipeline,pipeline的原理是客户端可以并行发送多个请求,这就解决了刚才串行请求的问题。这种并行处理请求的能力对提升性能的帮助非常大,但是它有什么缺点呢,缺点就是服务器的响应必须按次序返回,遵循先进先出。也就是说假设有3个请求可以同时从浏览器发送,但是服务器即使先处理完第3个请求,也必须在前两个请求响应完后才能发送第三个请求,这就是队首阻塞,队首阻塞好比我们去超市买单的时候,有几个队列,我选择了最短的一个,但是因为队首的那位顾客买了非常多东西,我等了很久,甚至比其他更长的队列还要久。就像下图右边所示,虽然是并行发起的请求,但是第一个请求花了很长的时间,第二三个请求只能在等待之后依次返回。
所以HTTP2就是为了解决这个问题诞生的,HTTP2可以并行发起请求,并行地返回,并且不需要遵循先进先出,可以乱序返回。如图上所示,如果第一个请求阻塞了,第二三个请求可以优先返回,可以看出来,HTTP2实际上实现了对TCP链路最大的利用。
这里怎么去验证HTTP2的多路复用确实有效,介绍一个工具来查看HTTP2的多路复用情况——webpagetest。我用这个工具打开我们的一个页面,查看connection view,在connection view里每一行代表一个TCP连接,在不支持HTTP2多路复用的情况下,可以看到同一个域名,一共打开了6个TCP连接,这里橙色这一段耗时代表的是建立TCP连接的耗时,那么建立了6个TCP连接带来的负面影响就是,初始化连接耗时也相应增大了。
我们再来看下在使用了HTTP2之后的connection view,打开同一个页面,可以看到只建立了一个TCP连接,在连接建立之后,所有的请求都在这个TCP连接上传输。相应的,TCP建立的耗时也只有一次,这是多路复用带来的好处。
HTTP2的另一个特性是头部压缩,以往HTTP1.1只会压缩内容部分,而对于头部是没有做压缩的。不要小看头部压缩这个特性,现在头部信息越来越多,user-agent、cookie、跨域头、CSP等等。我随意找了一个CSS,头部信息占了10%,所以头部压缩是很有必要的。HTTP2 压缩的原理简单说就是浏览器和服务端各自缓存了一份header 的映射表,来避免了重复header的传输,同时减小了需要传输的大小。
同样来看一下如何验证头部压缩的效果呢,可以使用一个空请求,也就是content-length=0,传输的size可以大致看作是头部的大小,可以看到使用HTTP2之后的头部比HTTP1.1减少了60%。
除此之外,HTTP2还有server push、请求优先级等特性,今天暂且不展开。介绍了HTTP2的一些特性,我们需要有一些实际的数据才能站得住脚。我们统计了QQ空间HTTP2的一些真实用户的数据。
从两个维度来分析HTTP2的性能数据,第一个维度是操作系统,上面蓝线是HTTP1.1,下面绿线是HTTP2,结论是不论哪种操作系统,HTTP2都要快些,而且安卓提升的多一些。
第二个维度是网络情况,在不同的网络情况下,HTTP2的速度都要比HTTP1.1快一些,而且在弱网络下差距会更大,可以说HTTP2对弱网络用户的体验非常大。
总体来看,不同的测速点,HTTP2比起HTTP1.1都有不同程度的提升。由于HTTP2多路复用的特性,当请求越多时,HTTP2的优势越明显,我们可以看到在JS加载和图片加载这两个时间点,HTTP2的提升最为明显,因为js和图片都是有多个请求并行加载的需求的,所以对于这两个测速点提升的比例也最高。
看起来HTTP2的数据还挺不错的,但是HTTP2性能上还有其他可提升的地方吗?
在研究HTTP2的时候,我看了一本书,叫《Web性能权威指南》,里面有一章是专门介绍HTTP2协议的,分析得很详细,其中有一句话是这么写的,“对HTTP2.0 而言,TCP 很可能就是下一个性能瓶颈”。文章里提到的TCP性能瓶颈是,HTTP2.0的多路复用其实会加剧TCP连接阻塞的可能性,如果TCP是下一个性能瓶颈,那还能用UDP协议吗?
这就回到最开始的QUIC的示意图,QUIC上面一层是HTTP2的API,也就是我们刚才了解的HTTP2的好处它都有。但是不一样的地方就是,QUIC在传输层用了UDP协议,而不是TCP协议。
QUIC,Q-U-I-C其中的U就代表UDP,QUIC之所以快,也是由于他采用了UDP,QUIC使用UDP我觉得有两个比较大的优势。
第一个是消灭了队首阻塞,前面在讲HTTP2的时候用超市结账的例子解释过什么叫队首阻塞,相信大家还记得,奇怪的是,刚刚明明说HTTP2解决了队首阻塞问题,为什么现在又要消灭队首阻塞呢?
我们看一下这张图,我们知道TCP/IP的五层模型,上面是应用层、和传输层,HTTP和TLS属于应用层,TCP属于传输层。HTTP存在的队首阻塞属于应用层的,我们用HTTP2解决了这个问题,但是传输层的TCP协议仍然存在着队首阻塞。基于一条TCP连接的HTTP2复用连接会面临这样的情况:当有丢包发生时,所有连接都将阻塞,这是由TCP的拥塞控制特性决定的,丢包必须恢复。相比较而言,在HTTP1的时候,当多条并行的HTTP连接中,有一条丢包,只会阻塞一条连接,这就是TCP层面的队首阻塞。所以针对传输层QUIC改用了UDP来解决队首阻塞问题。
第二个优势是0RTT建连,其实随着时间的推移,整个世界的带宽都在不断提升,但是受光速支配的往返时间是不会减少的,这使得我们要想办法来尽量减少RTT。QUIC将加密和传输的握手结合在一起,减少了建立一个安全的连接所需的RTT。QUIC连接通常是0-RTT的,意味着相比于TCP + TLS要快。
这是一个TCP+TLS握手过程,我们可以看到需要消耗几个RTT才能完成。QUIC是怎么减少这些RTT的呢?第一步,我们知道TCP必须经过三次握手才能建立一个可靠连接,但是UDP是不需要的,UDP不需要经过三次握手,第一次通信就可以直接发送数据,所以节省了TCP握手的时间。第二步,为了建立一个安全的连接,我们需要TLS进行加密。但是TLS握手也需要消耗多个RTT,虽然有一些session id、sessionticket、false start等手段,但是都存在各种各样的问题。而QUIC在第一次访问服务的情况下,需要一个RTT,来获得密钥交换算法和公钥等信息,同时会把这些相关信息作为server config缓存在客户端本地,第二次访问服务的时候可以实现0RTT建连,同时因为server config需要保存一定的时间,所以采用了基于DH算法的二级加密方法,保证了前向加密安全。不过这里TLS指的还是1.2版本的,TLS1.3的已经采用了QUIC的方案,也可以实现0RTT建连。
这是通常情况下QUIC发送数据的时序图,应用数据可以被立即发送而无需等待服务器的响应,节省了TCP和TLS握手的耗时。
我知道大家现在肯定有一个疑问,QUIC用了UDP带来那么多好处,但是肯定也有坏处啊,TCP的可靠性、连接性等等特性,QUIC不就没有了吗?实际上QUIC需要模仿实现TCP的一些功能,例如下面列的这些TCP的特性,QUIC都实现了,但并非照搬TCP协议的实现,而是做了一些改进。
可靠性方面QUIC是怎么解决的,我们都知道UDP是无状态不可靠的,如何解决丢包问题?QUIC采用了两个方式:前向纠错,举个例子,就是发送数据A和B,增加发送一个数据C等于A和B的异或,接收方接到这3个包的任意2个包,异或一下就可以得到第3个包;如果前向纠错不能恢复包,启用失败重发;
怎样将UDP转换成一个基于连接的协议也是QUIC要考虑的一个基本问题,。为什么需要基于连接,我们的网络环境随时在发生变化,经常会主动或者被动地断开一个连接。如果一个TCP连接被断开了,那么需要重新建立连接,又是一个三次握手,而QUIC使用一个GUID来标识每个连接,如果网络环境发生了变化,只要GUID不变,不需要重新建立连接和协商,仍然可以维持之前的上下文。
QUIC也实现了基于流级和连接级的流量控制。流相当于一个请求,例如一个js文件,连接则是所有请求共同复用的,也就是说一个连接上可能存在多个流。连接和流的流量控制的工作方式一样,但连接传送的字节和最大的接收偏移是所有流的总和。类似于TCP的滑动窗口,QUIC接收者将自己最多想要接收的数据的完整字节偏移发给发送者,同时通过WINDOW_UPDATE帧可以更新偏移量限制,通过BLOCKED 帧可以通知对端,发送方已经准备好发送数据了,但是当前被流量控制阻塞了。与TCP的区别在于,TCP窗口滑动取决于已经确认收到的字节数,而QUIC取决于最大偏移字节数,可以理解为,在丢包发生的情况下,QUIC可以接受更多的字节数。
QUIC协议当前默认使用了TCP协议的Cubic拥塞控制算法,同时也支持其他几种拥塞算法,算法上跟TCP没有太大区别,区别在于其他策略上。第一个,可插拔,这使得QUIC相对于TCP可以提供更丰富的信息用于拥塞控制算法,这样可以支持服务端针对不同的情况配置不同的拥塞控制算法;第二个,避免重传歧义,TCP如果发生超时重传情况,重传的包和之前的包都是同一个序列号,会导致客户端无法分辨收到的响应属于原始请求还是重传请求,而QUIC的序列号是递增的,避免了这个歧义,因为使用了相同的序列号,为了保证有序性,QUIC同时又引入了一个stream offset的概念,相当于用stream offset来控制顺序;第三个,QUIC的ACK帧最多支持了256个ACK块,可以理解为在丢包率比较高的网络下,可以减少重传量。还有其他的区别暂不一一列出。
简单讲了QUIC的原理之后,先来看看QUIC带来的性能提升。QQ空间目前主要在PC上使用了QUIC,QQ空间黄钻官网和游戏应用官网都已全量开启使用,QQ空间首页也在灰度开启中。因为我们目前是在PC侧来实践QUIC的,相比手机侧的话,PC侧的操作系统绝大部分都是windows,没有操作系统之分,网络环境也相对稳定,所以就不像刚才HTTP2一样分两个维度统计了,我们看一下总体的平均数据。我们从这四个测速点来看,QUIC的表现都比HTTP1和HTTP2要好一些,onload的时间提升了大约10%。
最佳实践
关于HTTP2和QUIC,我们现阶段总结了一些实践经验,希望大家交流一下。首先是关于如何部署。推荐两个比较简单的方式,一个是使用caddy部署,caddy是一个Go语言实现的通用Web服务器。它已经支持了QUIC,可以使用caddy进行服务端部署。第二种方式是使用腾讯云的CLB,腾讯的安全云网关团队在CLB上基于nginx实现了QUIC,新增了支持QUIC的库和第三方模块,也可以考虑直接使用其作为QUIC服务端。
QQ空间是怎么接入的,对于QQ空间这么庞大的业务来说,接入HTTP2和QUIC,我们考虑的是如何对业务更透明,接入更方便。我们现有的架构,在用户端和真实服务器中间有一层统一接入层。我们的HTTP和QUIC是在接入层统一实现的,接入层与用户端之间任意使用QUIC、HTTP/2、HTTP/1,而接入层与真实服务器之间还是使用HTTP1.1进行转发,这样对于我们的后端机器还是保持处理HTTP.1的请求即可。
部署了服务端之后,怎么知道浏览器支不支持HTTP2和QUIC协议,前面我们提到本地可以在chrome的开发者工具看出来,但是用户没办法查看开发者工具,支持度怎么统计。其实当前的请求需要使用什么协议返回,这个服务端是清楚地知道的,所以我们是通过服务端同时返回一个response header告诉浏览器端当前用的是什么协议,再在用户侧去统计这个header的值做上报。这里也提供两个检测页面,大家可以试一下自己的手机是否支持。
这是我们统计出来的用户手机侧的支持率,不论是安卓还是ios,HTTP2加SPDY的占比都接近80%。QUIC的占比在手机侧目前会比较低,因为只有安卓部分机型的chrome浏览器才支持,所以这里暂且没列入。
PC上目前HTTP2的支持率还是占了绝大部分。QUIC的支持度在1%左右,主要受限于浏览器以及网络环境,有些浏览器虽然是支持QUIC的,但是并没有默认开启,需要手动开启,这也影响了QUIC的支持度。虽然支持度不高,但是我是持乐观态度的,因为之前我们在刚开始使用HTTP2的时候,支持率只有不到20%,到现在已经远超过80%了,相信QUIC的支持率也会越来越高。
在统计性能数据方面,推荐大家从几个维度来统计。第一个是浏览器提供的performance timing,因为这是一个协议层面的优化,所以统计一些连接上的耗时对我们至关重要。Performance timing可以统计到连接时间和首字节接收时间等。第二个是服务器端提供的一些数据,有些数据浏览器上我们无法获知的,可以通过服务端返回的方式来统计,例如握手耗时、请求是否复用等等。第三个是JS和图片加载时间,由于多路复用的特性,当请求越多时,优势越明显,所以可以关注一下js和图片这种多个请求并行加载的耗时。第四个是用户环境,这个有助于我们分析不同环境下的数据。
调试工具方面,不论是HTTP2还是QUIC,都可以使用chrome的这个工具来查看,如果你对底层细节感兴趣,你可以看到所有的活跃连接,并且可以捕获每个包。
QUIC的使用中其实我们是遇到一些坑的。在本地测试的时候没发现什么问题,但是因为本地数据缺乏现实当中的多样性,难以据此确定我们带给用户的真实体验。在分析用户侧QUIC数据的过程中注意到,QUIC有时候比HTTP2还要慢得多,后来我们使用测速工具测试发现,在部分网络环境下,UDP的带宽要比TCP小得多,如果换一下网络环境,QUIC又比HTTP2快了,这其实是运营商做了一些限制。所以用QUIC协议有一个潜在的问题就是运营商的一些不可控因素。想要QUIC的性能比较好,需要一定的网络条件。第一个,因为QUIC使用443端口来处理UDP协议数据 ,所以要求网络必须要开放UDP的443端口,一些公司网络和校园网会禁用,不要问我怎么知道的,因为我们公司的开发网就是禁用的,所以QUIC的支持率除了要看浏览器,端口也是一个重要的影响因素。第二个,对于UDP没有限速,这就是我刚才举的例子,有些运营商或局域网会使用不一样的带宽分配,导致QUIC的速度不如HTTP。第三个,UDP丢包率没有异常,部分运营商有一个奇怪的现象,就是UDP的包会进行屏蔽或丢弃,这也影响了QUIC的性能。
前面说的三个问题,第一个问题我们可以交给浏览器,如果浏览器发现无法连通UDP的443端口,不会启用QUIC,会改用HTTP。但是第二、三个问题,作为web端来说很难发现和避免。所以目前,我们做了一些基于纯web的简单的检测手段。我们在开启QUIC的时候需要下发一个alt-svc的response header来告诉浏览器,表示下次请求可以使用QUIC。针对限速的情况,我们会用本地存储,分别记录他使用HTTP2和QUIC加载页面的耗时,如果QUIC明显慢于HTTP2的话,这个用户不会再下发alt-svc这个header,不会再使用QUIC,这个检测方法只对留存率比较高的页面有效。针对丢包的情况,我们会异步发送一个检测请求,测试用户QUIC的连通性,如果是超时无返回,也不会再下发alt-svc的header。因为现在主要在PC端使用,用户的网络环境相对稳定一些,如果移动端使用的话,网络环境切换的可能性非常大,也就是说我们的检测方法还需要随着QUIC的扩大使用不断地进行调整。
介绍了一些HTTP2和QUIC的经验之后,想再介绍一些不管是HTTP1.1、HTTP2还是QUIC都适用的策略。
域名分区是HTTP1时代比较有效的一个优化手段,在http1的时代,我们为了让多个请求能够并行发起,往往会把资源放在不同的域名下。但是在HTTP2和QUIC时代,这个优化的性能提升微不足道,因为HTTP2和QUIC本来就支持了请求并行,所以我们再也不需要用域名分区了。不过在多种协议并存的情况下,我们希望这两种情况可以共存。我们仍然可以使用多个域名,但是要保证几个域名都能解析到同一个服务器IP并且证书支持通配符或者多域名证书,这样在HTTP2下可以使用同一个连接,在HTTP1下仍然被视为域名分区。
HTTP1时代另一个优化手段是请求合并,多个文件合并在一起,减少请求数,目的是减少新建连接的初始化和握手,同样的,这个优化在HTTP2和QUIC的应用上已经没有必要。我们可以取消合并,这样做的好处是当你只改了一个小文件的时候,不需要更新整个大文件,这样能提升缓存利用率。在多种协议并存的情况下,我们让这两种策略并存,服务器返回当前所使用的协议时,我们根据不同的协议,会采用不同的加载方式。
在HTTP1.1的情况下,我们是使用js合并的,可以看到这里js的请求数一共有10个,并且每个js的大小都比较大,因为是合并后的。当然,为了让js的数量多一些,这里包括了一些异步操作之后加载的js。
同一个页面,在HTTP2的情况下,我们是取消了js合并的,可以看到这里js的请求数一共有39个,并且每个js文件大小比较小。这种请求分离并不会对加载速度造成影响,而且提升了缓存命中率。
还有一个优化策略是预建连接,预建连接是我们一个非常有效的举措,并且不管对于HTTP1、HTTP2和QUIC都是有提升效果的,我们的具体做法是在入口页面提前发起一个请求,在用户打开实际页面之前预建连接,平均能达到的连接复用率是75%,平均提升400ms的页面加载时间。
以上是关于从HTTP2到QUIC的主要内容,如果未能解决你的问题,请参考以下文章