80

HTTP/3就要来了,先看看我的解读

 5 years ago
source link: https://www.tuicool.com/articles/rEB3Qzn
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

然而技术的发展总是让人目不暇接,2018年10月,HTTP/3又发布了。虽然已经有一些中文技术媒体做了报道,但大多数是翻译的,而且内容大同小异。最近我专门学习了点关于HTTP/3的知识,在这里随便写写,和大家做个分享。

uU3uyu2.jpg!web

先简单回顾一下HTTP/2吧。自从1999年HTTP 1.1发布之后,Web一直在迅猛发展,可惜HTTP协议一直没有更新。等不及的Google自己搞了个SPDY(读音是“speedy”),并依靠Chrome浏览器大肆推广。看到SPDY的效果确实很好(可以带来近50%的性能提升),IETF推动制定了HTTP/2。 SPDY和HTTP/2的主要特性展示如下

fQJ7Bbu.jpg!web

如今HTTP/2已经不新鲜了,根据2019年2月对访问量最大的1000万个网站的统计,33.5%已经支持HTTP/2。在国内,如果你打开浏览器看看调试模式,会发现各大厂已经广泛使用HTTP/2,尤其是放置css、js、图片的资源站,HTTP/2基本是标配。这也很好理解,基本什么都不用做,就可以直接享受多路复用带来的好处,何乐而不为?

在传统HTTP中,概念模型非常简单:下层TCP通讯与上层HTTP完全不搭架,但TTP与TCP的“连接”是重合的,TCP传输的单位是packet,HTTP则采用request-response的模型。

在HTTP/2中,概念模型有所变化,HTTP/2中传输的基本单位是帧(frame)。与HTTP 1.1的明文传输不同的是,HTTP/2的帧是二进制的,同时TCP承载的“逻辑连接”叫数据流(stream),所有的状态流转、流控、优先级等等特性都是在数据流上实现的。HTTP/2中为大家所津津乐道的“多路复用”,简单说就是把数据流分解为多个帧,多个数据流的帧混合之后以同一个TCP连接来发送。

值得注意的是,HTTP有1.0和1.1的区分,所以写作HTTP 1.0,HTTP 1.1,但HTTP/2不会有其它小版本,所以不要写作HTTP 2.0,而应当写成HTTP/2。

虽然HTTP/2已经带来了巨大的性能提升,但大家对性能的渴求是没有止境的。在应用层的许多问题解决之后,下一个优化的重点就是传输层了。无论SPDY还是HTTP/2,传输层协议都是TCP,TCP有一些娘胎里带来的问题,比如慢启动,如果拥塞窗口尺寸设置不合理,TCP的性能会急剧下降。关于这个问题,网络上已经有许多讨论,这里不赘述。

另一个重要问题是,HTTP/2的多路复用带来的效果并不如想象的那么好。虽然HTTP/2中的传输连接可以多路复用,但仍然无法避免队头阻塞的情况出现。因为TCP是需要保证有序的,假如单个TCP连接同时承载了四路逻辑连接,其中某个逻辑连接丢包了,则其它三路都会受影响,都必须从丢包的时刻开始重传,这无疑是极大的浪费。测试表明,如果丢包率超过2%,那么HTTP/2甚至不如HTTP 1.1,因为HTTP 1.1中各连接物理隔离,不会互相影响。

fyyINbr.jpg!web

所以思路自然就是“改掉TCP的这些毛病”。考虑到现实中已经有成千上万的网络设备,它们只能识别TCP和UDP,软件不会进化,如果更新TCP协议当然不可行——虽然2014年12月发布了TCP的Fast Open,但现实应用中的情况并不让人满意。因此,可用的只有UDP了。对了,还有人考虑过SCTP,但SCTP在队头阻塞、TLS、四次握手等方面仍然存在缺陷,尚不能让人满意。

大概有人听过QUIC(读音quick),知道它是基于UDP的HTTP,也知道它依然是Google最先提出来的。确实,上次是Google率先搞出了SPDY,这次Google又率先搞出了QUIC。根据Google本意,QUIC是把传统的HTTP/TCP/IP协议栈中的TCP换成UDP(当然需要加密),能通过加密的UDP传输HTTP/2的帧。

按照Google的说法,这样的好处很多,比如UDP建立连接的延迟会低很多,而且避免了队头阻塞。除此之外,Google还提供了一个非常诱人的特性FEC(Forward Error Correction)。简单说,它想做到的是,一旦有packet丢失,接收方可以根据之前和之后的packet推断出丢失packet的数据,这样就避免了重传。但是这样必然要求增加冗余载荷,或者说,这就是网络协议中的RAID 5。按照目前看到的资料,其冗余比例大概是10%,也就是说,每10个pakcet中的冗余信息,就可以重构一个packet。

尽管Google的QUIC很先进,但QUIC不止这一家,IETF也有QUIC,如今已经改名HTTP/3,所以Google的QUIC有时候也写作gQUIC。与Google单纯在传输层动手,应用层基本沿用HTTP/2不同,IETF的QUIC是一个混合方案,既包括传输层的改动,也包括HTTP层的改动(比如全新的头部压缩)。从另一个角度来说,它更“完整”。虽然理论上QUIC也可以支持HTTP之外的其它上层应用,但目前这只是计划而已,第一版QUIC并不包含这方面内容。

在2018年11月,IETF正式宣布,HTTP-over-QUIC更名为HTTP/3。

nUF3quZ.jpg!web

本文讨论的是IETF版本的QUIC,Google已经宣布,会逐步把IETF的规范纳入自己的协议版本,实现相同的规范。

虽然TCP有各种问题,但换成UDP的话,TCP的不少功能也需要原样移植过来。许多人都知道,TCP是可靠的传输协议,而UDP是不可靠的。HTTP/3当然不能不可靠,所以它必须自己实现有序性、错误侦测、重传、拥塞控制、传输节奏调整等等特性。

HTTP/2“似乎”必须用到HTTPS,但规范并不强求HTTP/2使用HTTPS,也就是说,如果你用HTTP来跑HTTP/2,理论上也是可以成立的,虽然这有点怪异。

与此相反,QUIC的所有连接都是加密的,目前采用的是TLS 1.3。如果你仔细观察上面的图就会发现,TLS 1.3是“囊括”在QUIC当中的,也就是说,QUIC建立连接的握手过程当中就同时完成了加密握手。HTTP/3的握手很快,如果两台主机之间建立过连接,并且缓存了之前的secret,只要客户端验证之前缓存的server config就可以直接建立连接,相当于0-RTT,否则也只需要1-RTT就可以建立连接。此外,QUIC还容许在0-RTT的情况下从一开始就捎带数据,传统的“建立连接-加密握手-发送数据”如今可以三步并作一步(这个0-RTT和1-RTT的实现都非常有意思,有兴趣的话应当找资料来看看)。

QUIC中虽然也有连接(Connection),也基于IP和port建立,但它并不能直接与TCP的连接对应,也不同于HTTP/2中的连接。原因在于QUIC建立连接时既完成了经典的传输握手,又完成了加密握手——你可以认为这样分层责任不清晰,但它确实提升了效率。QUIC的连接与HTTP/2类似,一个物理连接也可以承载多个逻辑连接(也就是数据流)。但与HTTP/2不同的是,QUIC中的逻辑连接是彼此独立的,所以避免了TCP上出现的“逻辑连接甲丢包导致逻辑连接乙、丙、丁都需要重传”的情况。

QUIC连接的另一个特点是,每个连接都有一组连接ID。连接各端可以设定自己的连接ID,同时认可对方的连接ID。连接ID的作用在于从逻辑上标识当前连接。所以,如果用户的IP发生变化而连接ID没有变化,因为packet包含了网络ID标识符,所以只需要继续发送数据包就可以重新建立连接。而目前,如果用户的设备发生了网络切换,比如从Wi-Fi切换到4G,则所有连接都要断掉再重连。

如果你详细研究过HTTP/2,应当知道它的header压缩采用的HPACK,因为gzip做header压缩有安全性隐患。HTTP/3同样提供了header压缩,但不能直接沿用HPACK。原因在于,HPACK粗略来说就是一张动态表(dynamic table),由request-response共同维护它,后续header中不会完整重复之前的条目,而是引用之前的条目,TCP的有序性保证了它一定是先修改再读取,也就是先编码再解码。

然而如果使用HPACK,多个流的顺序是无法保证的,这样会导致解析错误。QUIC的解决方案是QPACK,其原理很简单:所有的header必须通过同一数据流来传输,而且必须严格有序。但是这样一来,从HTTP 1.1开始就困扰HTTP已久的队头阻塞又出现了。因此,QUIC的长期目标之一就是解决header的队头阻塞问题。

做过在线升级的朋友都知道,在线升级中的一个必须成分是提供降级方案,以保证“退化”兼容。无论HTTP/2还是HTTP/3,都不能逃避这部分的工作量。HTTP/2虽然可以通过upgrade这个header来升级,但也有更简单的办法,就是在TLS握手时协商HTTP的版本,比如Nginx就有NPN(Nginx Protocol Negotiation)扩展,自动协商协议,并已经被IETF采纳,成为ALPN(Application Layer Protocol Negotiation)。

如果web server有这样的特性,应用服务代码就不必为兼容HTTP 1.1和HTTP/2做太多工作。但是,如果应用程序中使用了Push等新特性,还是免不了要做很多事情。在业界,Google、Youtube、Wikipedia等大厂早已经提供了完整服务,HTTP/2和HTTP 1.1无缝切换,客户端完全无感知,它们的经验值得参考。

与HTTP/2不同的是,HTTP/3中新定义了一个header,可以用来指示客户端“在另一个端口提供了专用的HTTP/3服务”。

Alt-Svc: h3=":20003"

这个header说明,在本主机的20003端口开启了HTTP/3的服务。所以,客户端之后可以尝试和这个端口建立纯粹的HTTP/3连接。

聊了这么多QUIC的好处之后,再谈谈它的问题,有些观点来自我个人,未必足够准确客观,欢迎讨论。

虽然QUIC有这么多好处,但可以看到,相比HTTP/2,它的改动相当大,所以问题也不会少。

第一个问题是与遗留的网络设备的兼容问题。

基于目前的应用情况,许多网络设备对TCP和UDP的策略相当固定,TCP限制在常用端口,而UDP大概只开放了53端口(DNS)。所以如果HTTP/3使用UDP,兼容性方面可能会有不少问题需要解决。

不过如果这个问题可以解决,未来大概不会再出现这种问题,因为HTTP/3的设计思想中已经为未来做了考虑,应用层和传输层职责严格隔离,避免再出现“传输层一看端口就知道应用层在干什么”的情况。

第二个问题是QUIC的性能问题。

TCP虽然也是很老的协议,但应用广泛,操作系统内核中有对应的处理代码,BBR之类的新特性也可以大幅提升TCP的性能。但是QUIC放弃了TCP,据Google的文档,恰恰是因为TCP太稳定了,内核里的代码更新特别麻烦。此外,因为Linux内核设计之初并没有考虑多核的扩展问题,在多核(core)情况下反而会产生反复的陷核,造成进程阻塞,严重影响性能。

针对上面的问题,不少新的方案都把网络协议栈放到用户态处理,QUIC也顺应了这种大潮流。唯一的问题是,UDP的协议栈似乎还没有现成的让人满意的方案,或许我们还得再等待一段时间,才能用上可靠高效的方案。

第三个问题是服务端推送。

虽然很多人很想要这个特性,而且HTTP/2也确实加入了它,但关于它的应用仍然存在许多争议。简单说,HTTP/2的推送打破了HTTP传统的“一问一答”的通讯模式,在客户端没有请求的时候,服务端就可以给客户端发送数据,这难免被滥用(想想随处可见的那些最喜欢“在商言商”,最不喜欢谈“道德”的留言吧),尽管Chrome的开发人员说它们会检查推送并阻止恶意内容,那也是要在收到推送数据之后进行,这个方案并不完善。

同时,服务端也可能不顾客户端的缓存,执意重复推送,造成带宽浪费。HTTP/3保留了推送,但机制有所不同。客户端需要先同意,服务端才可以推送。而且,客户端可以设置服务端推送上限,超过上限的推送会出错。尽管如此,推送如何能妥善利用,目前还没有公认明确的答案。

最后一个问题来自调试和支持的工具。

任何技术要想大规模工程应用,靠“标准实现”单打一肯定是不行的,因为无法切片,无法细粒度调试。在经典的HTTP技术栈中,各层都有对应的工具,比如IP层有ping和traceroute,传输层有telnet,应用层有curl,正是有这些工具簇拥着,开发人员才可以很方便地定位问题所处的层次和细节。HTTP/2虽然有改动,但调试工具也不少,curl可以支持,还有nghttp2、h2c等工具,初步形成了完整的体系。HTTP/3的改动很大,如果没有对应的调试支持工具,可以想象部署和迁移都不会容易。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK