本站开启支持 QUIC 的方法与配置

在越来越讲究用户体验的今天,网络带宽的提高已经很难有显著的页面加载改善,而低延迟的优化往往能够起到意想不到的效果。在《TLS1.3/QUIC 是怎样做到 0-RTT 的》中我们分析了TLS1.3和QUIC在低延迟方面的原理和低延迟优势。在从源代码编译 nginx docker 镜像开启 TLS 1.3中我们已经把玩了TLS1.3,没有理由不把玩一下QUIC,对吧?

起初以为,在普及程度上,QUIC因为主要是Google主导,会曲高和寡。但是,查了一下,发现腾讯早在2017年就在生产环境应用了QUIC:让互联网更快的协议,QUIC在腾讯的实践及性能优化. 结果显示:

灰度实验的效果也非常明显,其中 quic 请求的首字节时间 (rspStart) 比 http2 平均减少 326ms, 性能提升约 25%; 这主要得益于 quic 的 0RTT 和 1RTT 握手时间,能够更早的发出请求。

此外 quic 请求发出的时间 (reqStart) 比 h2 平均减少 250ms; 另外 quic 请求页面加载完成的时间 (loadEnd) 平均减少 2s,由于整体页面比较复杂, 很多其它的资源加载阻塞,导致整体加载完成的时间比较长约 9s,性能提升比例约 22%。

既然大厂都已经发车,我司也就可以考虑跟进了。稳妥起见,决定先在自己的博客开启QUIC,然后再逐步在线上业务进行推广。

方案概览

方案非常简单:不支持QUIC的浏览器依旧通过nginx tcp 443访问;支持QUIC的浏览器通过caddy udp 443访问。

由于nginx近期没有支持QUIC的计划, 作为一名gopher, 因此这里选择caddy作为QUIC的反向代理。后面会介绍caddy的具体安装和配置方法。

对于支持QUIC的浏览器来说,第一次访问支持QUIC的网站时,会有一次服务发现的过程。服务发现的流程在QUIC Discovery
有详细介绍。概括来说,主要有以下几步:

  1. 通过TLS/TCP访问网站,浏览器检查网站返回的http header中是否包含alt-svc字段。
  2. 如果响应中含有头部:alt-svc: 'quic=":443"; ma=2592000; v="39"',则表明该网站的UDP 443端口支持QUIC协议,且支持的版本号是draft v39; max-age为2592000秒。
  3. 然后,浏览器会发起QUIC连接,在该连接建立前,http 请求依然通过TLS/TCP发送,一旦QUIC连接建立完成,后续请求则通过QUIC发送。
  4. 当QUIC连接不可用时,浏览器会采取5min, 10min的间隔检查QUIC连接是否可以恢复。如果无法恢复,则自动回落到TLS/TCP。

这里有一个比较坑的地方:对于同一个域名,TLS/TCP和QUIC必须使用相同的端口号才能成功开启QUIC。没有什么特殊的原因,提案里面就是这么写的。具体的讨论可以参见Why MUST a server use the same port for HTTP/QUIC?

从上面QUIC的发现过程可以看出,要在网站开启QUIC,主要涉及两个动作:

  1. 配置nginx, 添加alt-svc头部。
  2. 安装和配置QUIC反向代理服务。

配置nginx, 添加alt-svc头部

一行指令搞定:

安装QUIC反向代理服务器caddy

上面我们提到对于同一个域名,TLS/TCP和QUIC必须使用相同的端口号才能成功开启QUIC。然而,caddy服务器的QUIC特性无法单独开启,必须与TLS一起开启,悲剧的是TLS想要使用的TCP 443端口已经被nginx占用了😂

场面虽然有点尴尬,但是我们有docker:将caddy安装到docker中,然后只把本地的UDP 443端口映射到容器中即可。

于是我们创建了一个docker-caddy项目。Dockerfile 10行内搞定:

caddy 服务配置文件/conf/blog.conf:

启动docker:

开启Chrome浏览器QUIC特性

chrome://flags/中找到Experimental QUIC protocol, 设置为Enabled. 重启浏览器生效。

测试QUIC开启状态

重新访问本站https://liudanking.com, 然后在浏览器中打开:chrome://net-internals/#quic。如果你看到了QUIC sessins,则开启成功:

当然,你也可以给Chrome安装一个HTTP/2 and SPDY indicator(An indicator button for HTTP/2, SPDY and QUIC support by each website) 更加直观的观察网站对http/2, QUIC的支持情况。

从源代码编译 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

低延迟与用户体验杂谈

最近在做系统设计梳理的时候,明显感觉到「低延迟」已经成为被提及越来越频繁、考量权重越来越大的因素。 并且,越是靠用户近的系统,对延迟越敏感,对用户体验影响越大,对低延迟要求越高。

HTTP/2如今(2018.02)已经逐渐普及,其设计的第一目标就是降低延迟。主要采用了两个手段来解决:

  1. TCP连接复用。连接复用减少了TCP每次握手带来的延迟,同时避免了每次新建TCP连接的窗口慢启动带来的数据吞吐开启延迟。
  2. 使用数据分帧解决队头阻塞问题。当然,这个问题HTTP/2解决得不彻底,具体可以参见《当我们在谈论HTTP队头阻塞时,我们在谈论什么?》了解细节。

HTTP/2毕竟只能解决应用层的低延迟问题。如果要继续降级延迟,就需要下潜到传输层。因此,Google的QUIC和TLS 1.3应运而生。QUIC目前主要是Google主导,除去其自家的服务,如Google搜索首页,G+等,支持的网站还非常少。TLS 1.3则由标准化组织加持,目前在最新的OpenSSL、nginx已经支持。TLS 1.3能够做到新连接3RTT,恢复连接2RTT(TLS 1.2分别是4RTT, 3RTT;而2RTT已经与http的RTT持平!),的确非常吸引人。

回顾WEB技术过去十几年的发展,很多的特性引入和改进都是基于降低延迟。有些是技术层面的,比如上面提到的HTTP/2的低延迟设计、DNS查询缓存、HTTP1.1并发多个TCP连接请求资源、雪碧图等;有些是用户体验层面的,比如异步加载/预加载js资源、优先加载影响首屏渲染的CSS资源、避免使用大表布局、图片渐变加载、过渡动画等。

而这些年技术层面的发展其实都是受物理定律限制的。祭出程序员延迟心经Latency Numbers Every Programmer Should Know:

软件工程师无论是做什么职位和方向,心里都应该对此有B树。

有了这些当前人类所认识的物理极限,才能做到在「在边界内做事情」(这句话不是我说的,第一次看到这句话是吴军老师的《硅谷来信》)。

举个例子,当前人类认知范围内最快的速度是光速,这是一个上限,而广泛使用的光纤通信速度大概是光速的2/3。那么,要想优化上海到加州的网络延迟,无论你如何优化线缆布设以及质量,RTT都不可能低于127ms. 因此,如你所见,这些年虽然新架设了不少新的跨太平洋光缆,但是最优网络延迟没有什么太大变化,反倒是这些光缆带来的扩容让网络拥塞得到了缓解,让我们感觉出口网络「好像」更快了。

那么在光速这个物理边界的限制下,我们要如何降低延迟呢?显然,固定的两点之间的网络延迟是无法突破该边界的(这里不讨论空间扭曲力场🤦‍♀️)。但是,很多时候,我们要解决的问题是「让用户感觉延迟低」就可以了。

在这个思想下,CDN应运而生。将内容分发到距离用户近的网络节点来降低用户访问延迟。这个idea非常简单,甚至简陋,但是非常有效,并且廉价。

顺着这个思路下去,如果把算力分发到距离用户近的节点,那是不是也可以让用户觉得计算任务也变快了呢?在一定程度上,这是可以做到的。比如,我们使用了数十年运行于浏览器的javascript,以及我们的多IDC、多主架构方案,都有这方面的考量。近来流行的serverless、边缘计算其实也可以是认为是将算力部署到离用户尽可能近的物理位置或业务流程中。

有时候,技术参数指标的提升在短期内是难以低成本解决的。这个时候,尽量避免死磕参数,投入100%的精力去换取1%的性能提升。可以尝试从设计交互上给用户形成反应很快的体验,降低体感延迟。比如,1)耗时的任务后台化并给出进度条;2)区块处理的逻辑任务尝试修改成流处理,加速部分处理结果的输出,典型应用如以视频直播服务为代表的流媒体服务全力优化首屏播放延迟;批处理任务每处理完一个子任务就反馈结果等。

不仅软件系统的延迟影响着用户体验,硬件更是如此。现在人们普遍认为iPhone的用户体验是优于安卓的。因素有很多,那些复杂的系统参数普通用户未必搞得懂,但是很多用户却承认在滑动屏幕的时候iPhone比安卓更加「跟手指」,打开应用也「感觉更快」。「跟手指」这个体验跟苹果的软件+硬件的技术优化有关,目前安卓阵营也没有赶上。打开app快则是典型的交互优化:苹果打开app未必真的比安卓快,但是从点击app图标到显示story board的确非常快。无论苹果是否鸡贼,至少从这个层面看,它的确是非常了解延迟与用户体验之间的奥义的。至于苹果每次升级系统「故意」让老设备变卡……咳,咳,同学,你怎么有又抬杠呢😅

喜欢汽车并且喜欢驾驶的朋友一定知道甚至深入研究过这几款车:马自达3/6/BRZ/86-宝马M2/M4-保时捷718/911。排名分先后,每一辆都是不同级别的驾驶者之车,也是很多玩车朋友的玩车升级路线。这些车都是「运动取向」,这是一个非常抽象的概念,普通消费者未必明白,但是你会发现他们的消费者大多会用「操控好」「响应快」来评价这些车。如果你是参数党,你会发现其实这些车在同级别中都不是最好的,但却是给人愉悦感最强的。

由物及人,当上司交给你一个任务的时候,有反馈好过无反馈,快反馈好过慢反馈。

总结

在节奏日益变快的今天,低延迟很多时候意味着良好的用户体验。而做到低延迟可以通过技术优化,也可以通过一些产品的交互方式和有选择的强调某些方面来让用户感觉很快。内容比较分散,但是都是顺着这根主线,希望你也有所思考和收获。

参考文献

Introducing Zero Round Trip Time Resumption (0-RTT)