本站开启支持 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的支持情况。

Golang 获取 goroutine id 完全指南

在Golang中,每个goroutine协程都有一个goroutine id (goid),该goid没有向应用层暴露。但是,在很多场景下,开发者又希望使用goid作为唯一标识,将一个goroutine中的函数层级调用串联起来。比如,希望在一个http handler中将这个请求的每行日志都加上对应的goid以便于对这个请求处理过程进行跟踪和分析。

关于是否应该将goid暴露给应用层已经争论多年。基本上,Golang的开发者都一致认为不应该暴露goid(faq: document why there is no way to get a goroutine ID),主要有以下几点理由:

  1. goroutine设计理念是轻量,鼓励开发者使用多goroutine进行开发,不希望开发者通过goid做goroutine local storage或thread local storage(TLS)的事情;
  2. Golang开发者Brad认为TLS在C/C++实践中也问题多多,比如一些使用TLS的库,thread状态非常容易被非期望线程修改,导致crash.
  3. goroutine并不等价于thread, 开发者可以通过syscall获取thread id,因此根本不需要暴露goid.

官方也一直推荐使用context作为上下文关联的最佳实践。如果你还是想获取goid,下面是我整理的目前已知的所有获取它的方式,希望你想清楚了再使用。

  1. 通过stack信息获取goroutine id.
  2. 通过修改源代码获取goroutine id.
  3. 通过CGo获取goroutine id.
  4. 通过汇编获取goroutine id.
  5. 通过汇编获取goroutine id.

在开始介绍各种方法前,先看一下定义在src/runtime/runtime2.go中保存goroutine状态的g结构:

其中goid int64字段即为当前goroutine的id。

1. 通过stack信息获取goroutine id

原理非常简单,将stack中的文本信息”goroutine 1234″匹配出来。但是这种方式有两个问题:

  1. stack信息的格式随版本更新可能变化,甚至不再提供goroutine id,可靠性差。
  2. 性能较差,调用10000次消耗>50ms。

如果你只是想在个人项目中使用goid,这个方法是可以胜任的。维护和修改成本相对较低,且不需要引入任何第三方依赖。同时建议你就此打住,不要继续往下看了。

2. 通过修改源代码获取goroutine id

既然方法1效率较低,且不可靠,那么我们可以尝试直接修改源代码src/runtime/runtime2.go中添加Goid函数,将goid暴露给应用层:

这个方式能解决法1的两个问题,但是会导致你的程序只能在修改了源代码的机器上才能编译,没有移植性,并且每次go版本升级以后,都需要重新修改源代码,维护成本较高。

3. 通过CGo获取goroutine id

那么有没有性能好,同时不影响移植性,且维护成本低的方法呢?那就是来自Dave Cheney的CGo方式:

文件id.c:

文件id.go:

完整代码参见junk/id.

这种方法的问题在于你需要开启CGo, CGo存在一些缺点,具体可以参见这个大牛的cgo is not Go. 我相信在你绝大部分的工程项目中,你是不希望开启CGo的。

4. 通过汇编获取goroutine id

如果前面三种方法我们都不能接受,有没有第四种方法呢?那就是通过汇编获取goroutine id的方法。原理是:通过getg方法(汇编实现)获取到当前goroutine的g结构地址,根据偏移量计算出成员goid int的地址,然后取出该值即可。

项目goroutine实现了这种方法。需要说明的是,这种方法看似简单,实际上因为每个go版本几乎都会有针对g结构的调整,因此goid int64的偏移并不是固定的,更加复杂的是,go在编译的时候,传递的编译参数也会影响goid int64的偏移值,因此,这个项目的作者花了非常多精力来维护每个go版本g结构偏移的计算,详见hack目录。

这个方法性能好,原理清晰,实际使用上稳定性也不错(我们在部分不太重要的线上业务使用了这种方法)。但是,维护这个库也许真的太累了,最近发现作者将这个库标记为“DEPRECATED”,看来获取goroutine id是条越走越远的不归路?

5. 通过汇编获取goroutine id

虽然方法4从原理和实际应用上表现都不错,但是毕竟作者弃坑了。回到我们要解决的问题上:我们并不是真的一定要获取到goroutine id,我们只是想获取到goroutine的唯一标识。那么,从这个角度看的话,我们只需要解决goroutine标识唯一性的问题即可。

显然,上面作者也想清楚了这个问题。他新开了一个库go-tls, 这个库实现了goroutine local storage,其中获取goroutine id的方式是:用方法4的汇编获取goroutine的地址,然后自己管理和分配goroutine id。由于它获取到的并不是真正的goroutine id,因此我将之称为goroutine id。其实现的核心代码如下:

  1. 获取g结构地址。
  2. 分配伪goroutine id.

这种方式基本没有什么不能接受的hack实现,从原理上来说也更加安全。但是获取到不是你最开始想要的goroutine id,不知你能否接受?

小结

获取goroutine id是一条不归路,目前也没有完美的获取它的方式。如果你一定要使用goroutine id,先想清楚你要解决的问题是什么,如果没有必要,建议你不要走上这条不归路。尽早在团队中推广使用context, 越早使用越早脱离对goroutine id的留恋和挣扎。

Credit

Ubuntu 14.04开启nginx http2支持的方法

再过不到两个月,Ubuntu18.04就要出来了,但是手上还有一些老机器还停留在14.04?:

没有足够的时间和动力来升级这几台老机器,但是一些常用的软件准备顺手升级一下。最基本的自然是升级nginx支持http2. http2的优势可以参见《当我们在谈论HTTP队头阻塞时,我们在谈论什么?》以及《低延迟与用户体验杂谈》

Ubuntu 14.04开启nginx http2支持的前置条件

  1. nginx >=1.9.5
  2. openSSL >= 1.0.2

第一个条件大家一般都不会漏掉。但是第二个条件一般是http2无法成功开启时才发现。这是因为随14.04一起分发的openSSL版本是1.0.1f. 那么要开启http2支持,有两种方式:

  1. 使用他人编译好的ngingx with http2 support package安装;
  2. 升级本地openSSL版本,然后从源码编译安装。

而随Ubuntu 18.04一起分发的openSSL版本为1.1.0g, 因此不存在这个问题。

使用packaege安装(懒人专用)

这里需要注意一下,很多为提供14.04提供nginx安装包的源虽然可以让你安装更高版本的nginx,但是大多是使用openSSL 1.0.1编译的,因此无法支持http2, 比如jessis, nginx mainline为14.04提供的安装包是使用1.0.1编译的,因此不支持http2的。这里我们使用ondrej提供的源。

  • 卸载已经安装的nginx(会保留配置文件,take is easy):

  • 添加ondrej nginx安装源:

如果出现如下错误:

应该是终端编码问题,尝试使用LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/nginx解决。

  • 更新源,并安装:

从源码安装

修改nginx配置开启http2

设置nginx支持http2最关键是在https端口添加http2指令:

一个网站的参考配置模板如下:

小结

nginx开启http2的支持不要忘记了对openSSL最低版本的要求。其实http2已经不再时髦啦,http2+TLS1.3才是未来,可以参见《低延迟与用户体验杂谈》。后面会讲讲docker+nginx开启http2和TLS1.3的方式,这种方式即可以方便的尝试各个nginx版本,同时也不会破坏本地的nginx环境。