HTTP/3 已经箭在弦上,你准备好了吗?

去年的这个时候,国内的 web 网络环境开始普及和部署 HTTP/2. 时隔一年,HTTP/2 的普及程度有了显著提升,而各大CDN厂商普及的广度和速度一直走在行业前列。甚至有不少CDN厂商在直播以及部分HTTP场景还引入了 QUIC.

在拙文《当我们在谈论HTTP队头阻塞时,我们在谈论什么?》中,我们提到,HTTP/2 over QUIC 是当前唯一应用落地解决了传输层队头阻塞问题的HTTP实现。那个时候,无论是 HTTP/2 over TCP 还是 HTTP/2 over QUIC(UDP) 都被我们认为是 HTTP/2,只是传输层使用的协议不一样。这种略带暧昧的模糊叫法在2018年11月成为了历史:

在2018年10月28日的邮件列表讨论中,互联网工程任务组(IETF) HTTP和QUIC工作组主席Mark Nottingham提出了将HTTP-over-QUIC更名为HTTP/3的正式请求,以“明确地将其标识为HTTP语义的另一个绑定……使人们理解它与QUIC的不同”,并在最终确定并发布草案后,将QUIC工作组继承到HTTP工作组。在随后的几天讨论中,Mark Nottingham的提议得到了IETF成员的接受,他们在2018年11月给出了官方批准,认可HTTP-over-QUIC成为HTTP/3。

虽然看起来像是之前的 HTTP/2 over QUIC 换了一个名称(从我个人角度理解,取名为 HTTP/2.1也许更合适),但是其背后却体现了 IETF 对 HTTP 未来标准的态度和方向,也许几年以后来看这次名称的确立会更加明白其重要意义。

HTTP/3 与 HTTP/2 over QUIC 的区别

QUIC 将成为一个通用安全传输层协议

当前阶段,Google 实现的 QUIC 与 IETF 实现的 QUIC 是不兼容的。Google 版 QUIC 只能用于 HTTP/2,且在协议层面与 HTTP/2 有一些强绑定。如 QUIC 帧映射 HTTP/2 frame. 这就导致很多大厂都没有跟进 QUIC,使得 HTTP/2 over QUIC 基本只能在 Google 自家的 Chrome, Gmail 等软件中普及使用,一度给行业造成“只有Google在弄”的错觉。

纳入 IETF 以后,显然 Google 就不能这么玩了。QUIC 定位为一个通用安全传输层协议:

可以近似的认为 QUIC over UDP 将成为下一代(或替代)TLS over TCP. 也就是说, QUIC 将能应用于任何应用层协议中,只是当前阶段将优先在 HTTP 中进行应用和验证。

统一使用 TLS 1.3 作为安全协议

2018年,有几个重要的WEB标准终于尘埃落定,其中一个便是 RFC 8446 TLS 1.3. 这个标准对于降低延迟,改善用户体验,尤其是移动端的体验有非常重要的意义。在拙文《TLS1.3/QUIC 是怎样做到 0-RTT 的》中,我们提到 虽然 TLS 1.3和 QUIC 都能做到 0-RTT,从而降低延迟,但是 QUIC 却自顾自地实现了一套安全协议。主要是因为当时 TLS 1.3 标准还没有发布,而 QUIC 又需要一套安全协议:

The QUIC crypto protocol is the part of QUIC that provides transport security to a connection. The QUIC crypto protocol is destined to die. It will be replaced by TLS 1.3 in the future, but QUIC needed a crypto protocol before TLS 1.3 was even started.

如今,TLS 1.3 标准已经发布,而 HTTP/3 也纳入 IETF,因此 QUIC 也就顺理成章的使用 TLS 1.3 作为其安全协议。Google 在这些方面倒是从来都不鸡贼和墨迹,点赞。

使用 QHPACK 头部压缩代替 HPACK

其实,QPACK与HPACK的设计非常类似,单独提出QPACK主要是更好的适配QUIC,同时也是 Google 将 QUIC 从与 HTTP/2 的耦合中抽离出来,与 IETF 标准完成统一的必要一步。

HTTP/3 问题与挑战

UDP 连通性问题

几乎所有的电信运营商都会“歧视” UDP 数据包,原因也很容易理解,毕竟历史上几次臭名昭著的 DDoS 攻击都是基于 UDP 的。国内某城宽带在某些区域更是直接禁止了非53端口的UDP数据包,而其他运营商及IDC即使没有封禁UDP,也是对UDP进行严格限流的。这点上不太乐观,但是我们相信随着标准的普及和推广落地,运营商会逐步改变对UDP流量的歧视策略。国外的情况会稍好一些,根据Google的数据,他们部署的QUIC降级的比例不到10%。

QUIC 不支持明文传输

对于用户来说,这是一个优势,并不是问题。对于国内内容审查环境来说是个不可忽视的坎。但QUIC以后毕竟也是基于TLS协议的,国内HTTPS都能普及下来,QUIC的普及也许会更乐观一些。

UDP 消耗资源多

当前阶段,UDP消耗的CPU资源多,且处理速度慢。这是不争的事实,但是我相信随着UDP应用的增多,内核和硬件的优化一定会跟上,直至达到或超过TCP的性能。而 QUIC 因为实在应用层实现,因此迭代速度更快,部署和更新难度和代价更小,能够一定程度缓解如TCP那样的协议僵化问题。

进一步了解 HTTP/3

如果希望全面的了解 HTTP/3,推荐 Daniel Stenberg (CURL 作者)的 HTTP/3 explained, 如果不想看英文,可以翻阅 Yi Bai 同学翻译了中文版本HTTP/3详解

从源代码编译 nginx docker 镜像开启 TLS 1.3

nginx最近更新挺频繁的,其中TLS 1.3和是HTTP/2 Server Push是两个比较有意思的特性。前者可以有效的减少握手次数,降低延迟,尤其在恢复会话时候可以将握手开销降低到 0-RTT,后者则可以通过服务器主动推送资源,可以认为是在资源预加载之上更进一步的「资源主动加载」,有效提高网页性能和用户体验。

考虑到平时自己需要比较频繁的测试不同的nginx的新版本,但是又不想破坏机器本地已经安装好的nginx环境,因此我决定制作一个nginx docker镜像。需要说明的是,dockerhub官方是有一个nginx镜像的,但我们希望自己定制nginx版本和需要开启的模块,因此该镜像并不符合需求。

这里以开启TLS 1.3为例制作一个以ubuntu:16.04为基础镜像的 nginx dockerm镜像。顺便把上次在Ubuntu 14.04开启nginx http2支持的方法中埋的坑填了🙂

nginx开启TLS 1.3的前置条件

  1. nginx >= 1.13.0
  2. openSSL >= 1.1.1 alpha

openSSL版本有几点点需要注意:

  1. 以前可以使用分支tls1.3-draft-18编译支持TLS 1.3,但是现在已经merge到1.1.1版本中了,因此,这里不再推荐使用tls1.3-draft-18编译nginx. 使用openSSL 1.1.1编译nginx会触发nginx的一个config bug: undefined reference to 'pthread_atfork', 解决办法可以参见这里以及nginx mailing list, 这里暂时回退到使用tls1.3-draft-18.
  2. 不要尝试使用tls1.3-draft-19编译nginx, 使用Chrome 64测试无法在nginx 1.13.9上成功开启TLS 1.3, 应该跟Chrome对draft版本的支持有关系。
    • 2018-03-14更新:升级到Chrome 65以后,发现默认支持为TLS 1.3 Draft 23, 后面顺带连通config bug一起解决了再更新。
  3. openSSL目前最新版本是OpenSSL_1_1_1-pre2, 还不是stable版本,因此,不要在生产环境中使用。

Dockerfile

Nginx 开启 TLS1.3

一个完整的配置模板参考如下:

主要新增两个修改:

  1. ssl_protocols添加TLSv1.3.
  2. ssl_ciphers加入TLS13_为前缀的密码套件。

完整项目地址:docker-nginx

验证TLS 1.3

  • Chrome: 将 chrome://flags/ 中的 Maximum TLS version enabled 改为 Enabled (Draft).

如果你刚刚升级到了Chrome 65,选项中开启TLS1.3有三个选项:Enabled(Experiment 2), Enabled(Draft 22), Enabled (Draft 23). 同时很遗憾的告诉你,本文中使用的是Draft 18编译nginx,因此无法配合Chrome 65无法开启TLS1.3,请下面介绍Firefox的开启方法测试TLS1.3🤣

  • Firefox: 将 about:config 中的 security.tls.version.max 改为 4.

以Chrome 64为例,如果开启成功后Protocol显示TLS 1.3:

你也可以访问tls.liudanking.com来测试自己的浏览器是否已经成功开启TLS 1.3.

参考资料

本博客开始支持 TLS 1.3

TLS1.3/QUIC 是怎样做到 0-RTT 的

《低延迟与用户体验杂谈》中,我们提到了TLS1.3以及QUIC协议可以有效的降低传输层的延迟,改善用户体验。而TLS1.3和QUIC降低用户延迟有一个共同点,那就是在提供同等功能特性下,减少Round-Trip Time (RTT)次数。你甚至会经常听到QUIC和TLS1.3的拥趸说TLS1.3和QUIC可以做到0-RTT,基本消灭了(安全)传输层的延迟。事实真的是这样吗?

TLS RTT

我们先看一下TLS1.2 (以ECDH为例)的建立加密连接的过程:

从上图可以看出,在发送应用数据之前,TLS安全传输层需要经过2-RTT才能完成握手。如果是恢复会话,TLS1.2可以采用Session IDSession Ticket进行快速握手:

从上图可以看到,在使用Session Ticket的情况下,需要1-RTT。

那么TLS1.3是如何进一步优化的呢?

TLS 1.3完全握手

在完全握手情况下,TLS 1.3需要1-RTT建立连接。与TLS1.2有两点不同:握手过程中移除了ServerKeyExchangeClientKeyExchange, DH (Diffie-Hellman) 参数通过 key_share 传输。

TLS1.3恢复会话握手

TLS1.3恢复会话可以直接发送加密后的应用数据,不需要额外的TLS握手。因此,我们常说的TLS1.3 0-RTT握手其实是指恢复加密传输层会话不需要额外的RTT。但是,在第一次进行完全握手时,是需要1-RTT的。

与TLS1.2比较,无论是完全握手还是恢复会话,TLS1.3均比TLS1.2少1-RTT。因此,TLS1.3从被提上draft草案开始获得了各方面的好评。从nginx和Chrome支持此特性的速度可见一斑。

但是TLS1.3也并不完美。TLS 1.3 O-RTT无法保证前向安全性(Forward secrecy). 简单讲就是,如果当攻击者通过某种手段获取到了Session Ticket Key,那么该攻击者可以解密以前的加密数据。

这个问题不是TLS1.3独有,TLS1.2也存在这个问题。对于TLS1.3来说,要缓解该问题可以通过设置ServerConfiguration中的Expiration Date字段,使得与Session Ticket Key相关的DH静态参数在短时间内过期(一般几个小时)。

使用TLS最多的场景是HTTPS和HTTP/2. 以HTTP/2为例,一个完整的 HTTP Request需要经历的RTT如下:

HTTP/2 over TLS1.2首次连接 HTTP/2 over TLS1.2连接复用 HTTP/2 over TLS1.3首次连接 HTTP/2 over TLS1.3连接复用
DNS解析 1-RTT 0-RTT 1-RTT 0-RTT
TCP握手 1-RTT 0-RTT 1-RTT 0-RTT
TLS握手 2-RTT 1-RTT 1-RTT 0-RTT
HTTP Request 1-RTT 1-RTT 1-RTT 1-RTT
总计 5RTT 2-RTT 4-RTT 1-RTT

从上表可以看出:

  1. 首次连接发起HTTP请求是非常昂贵的。因此,如果你是用HTTPS作一些不可告人的代理应用的话,一定尽量保持长连接,避免频繁建立新连接。
  2. 连接的multlplexing(多路复用)可以有效减少RTT次数,降低延迟。在连接复用的情况下,TLS1.3将整个http request的RTT降低到了1个,达到理论的最小值。

QUIC RTT

  1. 当客户端首次发起QUIC连接时,客户端想服务器发送一个client hello消息,服务器回复一个server reject消息。该消息中有包括server config,类似于TLS1.3中的key_share交换。这需要产生1-RTT. 事实上,QUIC加密协议的作者也明确指出当前的QUIC加密协议是「注定要死掉的」(destined to die), 未来将会被TLS1.3代替。只是在QUIC提出来的时候,TLS1.3还没出生😂,这只是一个临时的加密方案。
  2. 当客户端获取到server config以后,就可以直接计算出密钥,发送应用数据了,可以认为是0-RTT。密钥推导可以参见Key derivation.
  3. 因此,QUIC握手除去首次连接需要产生1-RTT,理论上,后续握手都是0-RTT的。
  4. 假设1-RTT=100ms, QUIC建立安全连接连接的握手开销为0ms, 功能上等价于TCP+TLS, 但是握手开销比建立普通的TCP连接延迟都低:

    (正常体为首次建立连接的延迟,粗体部分为后续握手的延迟)

HTTP/2 over TLS 1.3 vs QUIC

当前,QUIC普遍的应用场景是 HTTP/2 over QUIC. 我们以一个完整的 HTTP Request需要经历的RTT为例,比较TLS1.3与QUIC的RTT开销:

HTTP/2 over TLS1.3首次连接 HTTP/2 over TLS1.3连接复用 HTTP/2 over QUIC首次连接 HTTP/2 over QUIC连接复用
DNS解析 1-RTT 0-RTT 1-RTT 0-RTT
TCP握手 1-RTT 0-RTT
TLS握手 1-RTT 0-RTT
QUIC握手 1-RTT 0-RTT
HTTP Request 1-RTT 1-RTT 1-RTT 1-RTT
总计 4RTT 1-RTT 3-RTT 1-RTT

从上表可以看出:HTTP/2 over QUIC相比HTTP/2 over TLS 1.3最大的优势是首次连接是的RTT开销降低了1-RTT(25%)。在连接复用的情况,QUIC的RTT开销与TLS1.3相等。

前面我们提到,QUIC(UDP+QUIC Crypto)在功能层面等价于TCP+TLS, 并且其加密协议(QUIC Crypto)未来会被TLS1.3代替。而在连接复用的情况下QUIC与TLS1.3的RTT开销旗鼓相当,那么是否意味着在HTTP/2的应用场景下,TCP+TLS1.3就可以完全替代QUIC呢?答案是否定的,主要原因如下:

  1. QUIC从数据包级别解决了队头阻塞的问题,而TCP+TLS只能解决http request级别的队头阻塞问题。具体原理可以参见《当我们在谈论HTTP队头阻塞时,我们在谈论什么》.
  2. QUIC与HTTP/2结合更加紧密,比如HTTP/2的stream, frame, Header Compression都可以直接映射到QUIC的stream, packet, Header Compression. 从协议分层看这会使得应用层和传输层紧耦合,但是在HTTP/2这个具体场景下,QUIC可以接管HTTP/2的拥塞控制,相较于HTTP/2 over TLS 1.3会受到TCP和HTTP/2两层的拥塞控制来说,无疑具有更直接有效的控制策略。并且,QUIC的拥塞控制是应用层可插件化的。虽然你也可以为TCP编写拥塞控制模块,但那是一个内核模块,稍不注意就会让系统崩溃。如果你有兴趣的话,实现一个QUIC的BBR拥塞控制模块是很容易的。
  3. QUIC支持连接迁移(Connection Migration),即在客户端切换网络IP以后,也能够保持连接不间断。这对于移动网络这种漫游网络特别友好,再加上QUIC在首次连接上RTT比TLS1.3低25%,因此会有更好的用户体验。虽然Multipath TCP, MPTCP也可以吃支持连接迁移,但这受限于操作系统和网络协议栈,要想普及,任重道远。
  4. QUIC支持Forward Error Correction,利用一定的数据包冗余,提供丢包时恢复丢包数据的能力。减少了包重传的数量,进而减少延迟,提高带宽利用率。如果你使用过kcptun,你会发现它也采用了FEC,对于弱网和高丢包环境下,效果尤其好。

当然,QUIC也是有缺点的。比如很多运营商对UDP的QoS级别是比TCP低的,甚至只允许DNS 53端口的UDP包,其他UDP包都是被封掉的。在IDC中,UDP的优先级一般也比TCP低。因此,即使以后大规模普及QUIC,也不会完全替代TCP+TLS,毕竟需要确保在QUIC不可用时,可以回落到TCP+TLS.

总结

在首次连接的时候,无论是TLS1.3还是QUIC都需要1-RTT,在连接复用的情况下,两者才能做到0-RTT. 在HTTP/2的应用场景中,QUIC可以有效的降低首次连接的RTT次数,并且支持连接迁移、FEC以及更加灵活高效的拥塞控制,因此可以在移动网络、弱网环境提供低延迟的用户体验。

扩展阅读