http://mp.weixin.qq.com/s/AEuvTp6qvOBiA9HtLevTrA

英文原文:https://www.nginx.com/blog/nginx-1-13-9-http2-server-push/

译者:诗书塞外

我们很高兴地宣布,Nginx 1.13.9 于2018年2月20日正式发布,这个版本支持HTTP/2的服务器端推送。对于Nginx PLUS的用户,HTTP/2服务器端推送的支持将包含在即将到来的 Nginx PLUS R15版本,这个版本预计于四月份发布。

服务器端推送,是HTTP/2标准定义的功能。这个功能允许服务器预判客户端可能会用到的资源,并提前将资源推送给客户端。这样,我们就可以减少页面加载时间,为用户提供更流畅的体验。

服务器端推送可以为客户端预先准备样式表、图片等页面加载需要的文件。你需要仔细甄别哪些资源需要推送,尽量推送客户端确实需要请求的,避免推送客户端已经缓存的资源。

在这篇博文中,我们将描述:

HTTP/2服务器端推送的基本配置

如何验证HTTP/2服务器端推送生效了(使用浏览器工具或者nghttp)

使用响应头Link自动推送内容

选择性推送内容

衡量HTTP/2服务器推送的效果

配置HTTP/2服务器端推送

要在页面加载时推送资源,使用http2_push指令:
server {

# Ensure that HTTP/2 is enabled for the server
listen 443 ssl http2;


ssl_certificate ssl/certificate.pem;
ssl_certificate_key ssl/key.pem;

root /var/www/html;

# whenever a client requests demo.html, also push
# /style.css, /image1.jpg and /image2.jpg
location = /demo.html {
    http2_push /style.css;
    http2_push /image1.jpg;
    http2_push /image2.jpg;
}

}

验证HTTP/2服务器端推送

你可以用下面两种方法轻松地验证服务器端推送生效了:

浏览器的开发者工具

HTTP/2的命令行工具,比如nghttp

使用开发者工具验证(以Google Chrome为例)

下面是如何使用浏览器的开发者工具验证服务器端推送是生效的,这里使用Chrome作为例子。在下图中,开发者工具中Network页中的Initiator列显示出我们通过服务器端推送,将几个资源伴随对/demo.html的请求一同返回给了客户端。

通过命令行工具验证(nghttp)

除了浏览器的开发者工具,你还可以使用命令行工具nghttp来验证服务器端推送是有效的。你可以从github上下载并安装nghttp,也可以使用操作系统的软件包管理工具来安装(比如在Ubuntu中,使用apt-get安装nghttp2-client)。

$ nghttp -ans https://example.com/demo.html

id responseEnd requestStart process code size request path
13 +84.25ms +136us 84.11ms 200 492 /demo.html
2 +84.33ms +84.09ms 246us 200 266 /style.css
4 +261.94ms
+84.12ms 177.83ms 200 40K /image2.jpg
6 +685.95ms * +84.12ms 601.82ms 200 173K /image1.jpg

图中的星号代表资源是由服务器端推送的。

自动推送资源给客户端

很多时候,把需要推送的资源列举在nginx配置中不是一个很好的做法。因此,Ngnix也支持解析Link预加载响应头,然后自动推送响应头中提到的资源。想要开启预加载,在配置中开启http2_push_preload指令。

server {

# Ensure that HTTP/2 is enabled for the server
listen 443 ssl http2;


ssl_certificate ssl/certificate.pem;
ssl_certificate_key ssl/key.pem;

root /var/www/html;

# Intercept Link header and initiate requested Pushes
location = /myapp {
    proxy_pass http://upstream;
    http2_push_preload on;
}

}

比如,使用Nginx做请求代理(HTTP,FastCGI,或者其他请求类型)时,上游服务器upstream可以在返回的响应头中增加一个Link字段。

Link: ; as=style; rel=preload;

Nginx解析这个头部字段内容,并自动推送资源/style.css。
Link字段中的路径必须是绝对路径,可以带查询参数。

想要推送多个资源,你可以在头部添加多个Link字段,或者只用一个Link,但用逗号分隔多个资源说明。

Link ; as=style; rel=preload, ; as=image; rel=preload;

如果你不想让Nginx推送一个已经加载过的资源,向头部加一个nopush参数

Resource is not pushed

Link: ; as=image; rel=preload; nopush;

当http2_push_preload已经开启,你也可以在nginx配置中添加Link响应头来推送资源。

add_header Link “; as=style; rel=preload;”;

选择性推送资源给客户端

HTTP/2的标准中没有解决什么资源应该推送、什么资源不需推送的问题。不过很显然,推送的资源最好不是已经缓存过的内容,否则这次推送就没有意义了。

一个可行的方案是,只在客户端第一次访问网站时推送资源。你可以在程序中检查是否有该客户端对应的session存在,只有在没有session时才推送,换言之,也就是选择性地推送资源。

假设客户端设计良好,在首次之后的请求中都会携带session cookie,那么这个方案就能保证每一个资源只在第一次请求时被推送一次。

server {
listen 443 ssl http2 default_server;

ssl_certificate ssl/certificate.pem;
ssl_certificate_key ssl/key.pem;

root /var/www/html;
http2_push_preload on;

location = /demo.html {
    add_header Set-Cookie "session=1";
    add_header Link $resources;
}

}

map $http_cookie $resources {
“~*session=1” “”;
default “; as=style; rel=preload, ; as=image; rel=preload, ; as=style; rel=preload”;
}

测试服务器端推送的效果

为了衡量服务器端推送的效果,我们创建一个简单的页面/demo.html,它引用了一个独立的css文件/style.css,这个css又引用了两个图片,我们使用三种不同的配置测试页面加载的速度:HTTP, HTTPS, HTTP/2。

Sequential GETs (No optimization) – The browser loaded resources when it discovered they were required

Preload Hints – Preload hints (Link headers) were included in the first response to tell the browser to load the dependencies

Server Push (HTTP/2 only) – Dependencies were pre‑emptively pushed to the browser

我们将进行多个测试:

HTTP使用多个GET请求请求资源,浏览器在发现需呀某个资源时再进行请求。

有preload标识的HTTP在第一次请求返回后得到preload标识,开始让浏览器加载这些依赖资源。

HTTP/2的服务器端推送会预先将资源直接推送给浏览器。

它们的行为是由Chrome的开发者工具衡量的。我们多次重复,取出每一种配置的典型性能,并根据RTT值(由ping得到)进行加权调整后得到下面的数据:

一些基本的观察

建立HTTP链接需要一个RTT时间,建立HTTPS或者HTTP/2链接需要两个RTT时间。

由于HTTP和CSS的资源体积小于一个MTU值,所以一个GET操作在一个RTT时间就可以完成。

当DOM loaded事件发生时,才真正地发起一个请求,来请求需要的资源。

解释结果:预加载

服务器推送为预加载减少了一个RTT时间,推送资源是在第一个请求返回时同步返回的,而preload预加载资源则需要额外花一个RTT的时间,其中第一个请求返回需要0.5个RTT,preload使用的GET请求到达需要0.5个RTT时间。

测试备注

我们进行了多次测量。每次测试开始时浏览器都没有缓存,也没有到nginx的keepalived链接。我们为nginx配置了keepalive_timeout和http2_idle_timeout,以便快速关闭keepalived链接。

无论使用preload标签还是服务器端推送,字体都无法有效地预加载,这是一个Chrome的已知问题。即便一个字体已经加载过了,Chrome还是会重复地请求字体资源。

需要注意的是,测试开始前一定要清楚缓存,并在返回的响应中设置缓存过期时间控制头。

Chrome会缓存preload预加载的资源。即便你禁用缓存,这些缓存还是会生效,就算你明确地清除浏览器缓存,实际上缓存也不是每次都能被清除。Chrome有时也会不顾缓存地存在而去加载资源,这些加载请求在测试结果中我们已经剔除了。

我们手动设置了/etc/hosts,所以测试过程中不受DNS延迟的影响。

我们没有测试在已经有缓存的情况下的效果。我们测试过程中,每次测试都是清除了缓存的,服务器端推送进行得太快,根本没机会在请求中中断请求。

结论

这些测试都很简单,主要用来讲诉preload标签和服务器端推送的机制。在简单的情形下,服务器端推送相比于preload标签,可以带来一个RTT时间的优化,在很多未优化过的情境中,可能带来的优化效果更明显。

真实使用场景中有更多的变量。有更多可能用到的资源,也有更大的机会,会因为你错误地使用服务器推送而浪费了带宽。浏览器间的不一致也会影响最终的效果,你需要做的工作肯定比这个例子复杂得多。

比如,Chrome团队撰写了一个服务器端推送配置推荐细则,他们在复杂的网站上测试了各种优化配置的效果。如果你想部署HTTP/2的服务器端推送,那么他们的报告《Rules of Thumb for HTTP/2 Push》非常值得一读。

最后实际的结论就是,HTTP/2服务器端推送让你可以预先制定哪些资源需要推送加载,相比于返回一个preload提示,服务器端推送有切实可见的性能提升。当然,错误的配置可能会导致你浪费带宽,所以上线前请认真评估和检查你的配置。