关于 HTTP/3 的一些记录

HTTP/3 即将被标准化。为了了解 HTTP/3 ,我将做一些笔记。

谷歌拥有世界上最流行的浏览器 Chrome,还有两个最流行的网站之一的 Google.com 和 Youtube.com 。因而他们几乎控制着未来网络传输协议的开发过程。他们对协议的第一次升级是一个叫做 SPDY 的协议,这个协议最终被标准化为 HTTP/2 。他们的第二次升级叫做 QUIC ,即将标准化为 HTTP/3 。

SPDY 已经被主流浏览器(Chrome, Firefox, Edge, Safari)和主流服务器(Apache, Nginx,IIS,CloudFlare)所支持。除了 Google 自己的网站之后,还有许多流行的网站也支持了 HTTP/2。然而你并不能给 HTTP/2 抓包,因为它往往是通过 SSL 加密传输的。尽管标准协议中允许 HTTP/2 裸跑在 TCP 上,但是所有的实现方式都会给它加上 SSL。

有关‘标准’,我们先做一点预习。在互联网之外的世界,‘标准’通常是由政府部门来管理,由权益相关的人坐在一起仔细讨论得出来,然后通过法律法规约束让大家一起使用的。在互联网世界里则不一样,人们通常会先实现协议,然后看别人是不是喜欢这个,如果别人喜欢的话,也会开始着手使用。标准往往是事实上的, rfc 是为已经在互联网世界上很好地工作的东西而编写的,用来记录人们已经在使用的东西。SPDY 首先是被浏览器服务器先使用的,而非事先标准化才使用的,直到被一些核心用户开始把 SPDY 添加进 rfc ,才有后来的标准化。QUIC 也是如此,在被标准化为 HTTP/3 之前,它已经在被使用了,而不是现有标准化这个里程碑,大家才开始使用它。

QUIC 原则上更像是一个新版本的 TCP(TCP/2 ???) 而非是新版的 HTTP (HTTP/3) 。它并没有真正改变 HTTP/2 的工作方式,而是改变了传输层的工作方式。因此,我下面的内容将会更加集中于传输层的内容,而非 HTTP 问题。

QUIC 主要的特性是有了更快的连接速度和等待时间。TCP 需要在建立连接之前发送大量数据包来确认连接。SSL 同样需要在建立连接之前发送大量的数据包来确认加密。假设网络有一些延迟,比如人们在使用卫星流量的时候,会有半秒的 ping 连接时间,这将会需要更多的时间来建立连接。通过减少往返,连接可以更快地进行,以便当您单击链接时, 链接的资源会立即弹出。

下一个重要的特性是带宽。网络连接的源和目标之间总是会有带宽限制的存在,这几乎总是由于拥塞造成的。双方都需要发现网络带宽的速度,以便能够及时地传送数据包。如果数据包发送太快,就会发生丢包问题,这样会在会在没有提高传输速率的情况下给其他人带来更大的拥塞。发送数据包太慢则意味着网络使用率不佳。

之前的 HTTP 这点做得不好。使用单一 TCP 连接对 HTTP 不起作用,因为用户和网站交互的时候往往需要传送多个内容,因此浏览器和服务器之间需要打开多个连接(通常为6个)。但是这样会破坏带宽估计,因为每一个 TCP 连接都在独自尝试运行,就像其他连接不存在一样。SPDY 通过连接的多路复用解决了这个问题,它将浏览器和服务器之间的多个单一连接合并到了一条 TCP 连接中。

QUIC 扩展了多路复用的思路,使浏览器和服务器中的多个交互更加容易,在同一个带宽下,不会有一个连接影响到另外一个连接的情况。从用户的角度来看,用户在访问网络的时候有更加流畅的连接速度,并且会比以往有更少的拥堵。

接下来我们来说用户态堆栈。TCP 有一个问题,尤其在服务器上更加突出,TCP 连接是由操作系统内核来处理的,而 web 服务器是运行在用户态下的。跨内核边界传输内容往往会导致性能问题。跟踪处理大量的 TCP 连接会有可伸缩性问题。一些人曾经尝试把服务器放到内核态里面运行,来避免内核态用户态之间的数据转换,这个办法其实并不好,因为这破坏了操作系统的稳定性。从我的角度来讲,BlackICE IPS 和 masscan 两个产品使用用户态的驱动来执行硬件,从用户态的的进程来获取网络包,这样可以绕过内核态的处理,使用自己的 TCP 栈。近年来这套方案在 DPDK 套件中变得越来越流行。

但是把 TCP 切换到 UDP ,你将会获得类似的性能提升,而且不必使用用户态的驱动。你可以调用 recvmsg() 函数来同时接受一组 UDP 数据包,而非使用众所周知的 recv() 函数来一次接收单个数据包。它仍然会有用户态内核态之间转换的问题,但是它一次性接受了上百个数据包,而非一个数据包转换一次。

经过我自己测试,使用常用的 recv() 函数,你可以被限制接受在大约 500,000 个 UDP 包每秒,但是使用 recvmsg() 函数以及做了其他的一些优化(比如说多核使用 RSS )之后,你可以在低端四核服务器上每秒接受 5,000,000 个 UDP 包。由于充分使用了每一个内核,如果移动到 64 核服务器上的话,这将会有更加巨大的提升。

顺便提一下,“RSS” 是网络硬件的一个功能,可以将传入的网络包拆分进多个接收队列里面。多核可伸缩性最大的问题是,当两个 CPU 核心需要同时修改或者读取同一个内容的时候,共享同一个 UDP 数据包队列是最大的瓶颈。因此英特尔和其他网络供应商带头添加了 RSS 功能,可以给每个 CPU 内核他们自己独享的数据包队列。Linux 和其他的操作系统升级了 UDP 的实现,支持了单个 socket (SO_REUSEPORT) 连接可以有多个文件描述符,以便可以同时处理多个数据包队列。现在 QUIC 使用了这个高级的功能,允许每个核心可以管理它自己的 UDP 数据包,而不会出现于其他内核出现竞态竞争共享数据包的伸缩性问题。我之所以提到这一点是因为,我个人曾经在2000年的时候与英特尔硬件工程师讨论了当时出现的多个数据包队列的问题。在过去的二十年里,这是一个很常见的问题,现在有了一个很明白的解决方法,一直到它作为 HTTP/3 尖端功能,看着它一直发展很有趣。如果没有网络硬件中的 RSS ,可能就不会有 QUIC 成为标准协议的一天。

QUIC 中还有另外一个很酷的解决方案是移动端支持良好。当你使用你的笔记本电脑切换不同的 wifi 网络时,或者使用你的手机上网是,你的 IP 地址可能会发生变化。操作系统和网络协议不会正常的关闭之前的连接再打开新的连接。但是有了 QUIC 之后,网络连接的标识符将不会像是 socket 的传统概念(源地址/目标地址 port 地址协议的组合)了,取而代之的是一个分配给这个连接 的 64 位标识符。

这表示,当你移动你的网络的时候,即便是你的 IP 地址发生了变化,你仍然可以从 YouTube 上不间断地获取视频,或者可以在不挂断电话的情况下仍然可以继续保持视频通话。几十年来互联网工程师一直在为 “移动 IP” 苦苦纠缠,试图想出一个可行的解决方案。他们专注于端对端原则,即在你移动是仍然以某种方式保持一个恒定的 IP 地址,这并不是一个好的解决方法。非常高兴见到了 QUIC HTTP/3 终于解决了这个问题,在现实世界中终于有个一个可以用的解决方案。

如何使用传输协议呢?几十年以来,网络编程的标准一直是 ‘socket’ 传输层 api。你可以在你的代码中调用类似于 recv() 的函数来接受数据包。有个 QUIC / HTTP/3 之后,我们不在需要调用操作系统提供的传输层 api 了。取而代之的是,你可以在更高的层级中使用它,比如说,你可以在 go 语言中使用,或者通过 Lua 在 OpenResty nginx 之类的服务器中使用它。

我之所以提到这一点是因为你有关 OSI 模型的学习教育中,缺少的一点是,它最初设想每个人都只会编写应用层的 API 而不是传输层的 API 。然而应该会有另外的情况,比如说,应用程序的服务组件(中间件),他们会以一种标准的方案(像文件传输,消息传递等等)来为不同的应用服务。我认为人们越来越多地转向这种模式,特别是在 Google , QUIC ,protobufs 等等的推动下。

谷歌和微软之间有一种反差。微软有一个流行的操作系统,所以他的创新是在他们的操作系统内部可以做什么来驱动的。谷歌的创新是操作系统顶层还能做什么改进或者创新决定的。还有脸书和亚马逊,他们必须在谷歌提供给他们的技术栈之上或者之外创新。世界五大公司一次是苹果谷歌微软亚马逊脸书,因此在哪里推动创新是很重要的。。。(原作者发牢骚)

总结:

当 TCP 协议在 1970s 创建的时候,是很出众的。它处理的事情,比如说拥堵控制,比跟它竞争的协议好上不止半点。尽管人们并没有预料到 IPV4 地址会拥有超过 40 亿个这样的事情,可以说现代的互联网远远比七八十年代预料的要好。从 IPv4 到 IPv6 的升级在很大程度上保持了IP的优势。从TCP到 QUIC 的升级同样基于 TCP 的优点,但将其扩展到满足现代需求。 实际上,令人惊讶的是,TCP已经持续使用了这么长时间,没有任何升级,仍然运行良好。


原文: Some notes about HTTP/3 —— By Errata Security