阅读 280

再谈ServerPush,Push or Not Push

互联网工程任务组(IETF)于2014年12月将HTTP/2标准提议递交至互联网工程指导组(IESG)进行讨论,于2015年2月17日被批准,于2015年5月以RFC 7540正式发表。-维基百科

回过头来看,距离HTTP/2标准发布已经过去了五年。这五年,前端发生了技术翻天覆地的变化: IE浏览器逐渐式微,MVVM框架的兴起和繁盛,WebAssembly的萌芽。

这五年,HTTP/2 的普及和推开也无比迅速。

根据最新的数据,HTTP/2的浏览器支持程度已经来到了令人振奋的96%

再看HTTP/2

HTTP/2 相较于 HTTP1.x 有哪些改进?

上面这个问题已经成为前端面试的必考题。二进制分帧、多路复用、头部压缩、服务端推送这几个关键词大家已经熟的不能再熟。

HTTP/2带来了显而易见的性能提升,又有着如此高的浏览器支持度,和极低的使用成本,不用简直天理难容。目前中国各大网站都使用 HTTP/2 提升网站加载性能和用户体验。

但我们再看HTTP/2的几大特性,二进制分帧、多路复用、头部压缩这几个自不必多说,只要开启了HTTP/2,就可以使用这些特性,给网站带来性能优化。那么,Server Push去哪了?

我们找遍了中国各大门户网站,都没有看到Server Push的身影。问题出在哪?

Why not Push

简单回顾一下Server Push的概念:

服务器推送(server push)指的是,还没有收到浏览器的请求,服务器就把各种资源推送给浏览器。

上面这张图片看起来很美妙,当使用Server Push时,CSS和HTML一起返回,从而减少了一个RTT的时间

让我们试着解决第一个问题:

为什么Server Push没有得到广泛的应用?

需要配置?

ServerPush是HTTP/2 协议里面唯一一个需要开发者自己配置的功能,其他功能都是服务器和浏览器自动实现,不需要开发者关心。

是否是因为需要配置,才使得Server Push难以推广?

Nginx和Apache等网关早已支持Server Push功能,只需要简单配置后,在HTML的Response的Header中带上:

Link: ; rel=preload; as=style
复制代码

便可以轻松实现ServerPush😁。

一两句简单的配置,无法阻挡开发者对于极致性能的追求,那么还有其他原因吗?

CDN带来的痛

上图中Without Push和With Push对比只是最简单的场景,基本不可用于实际的生产中。

在实际的场景中,为了提升加载速度和减轻服务器的负载,一般会使用CDN进行资源的加载。于是乎,我们的加载时序图就会变成这样:

似乎也不复杂,只要我们的CDN支持Server Push,同Nginx一样,带上相应的Header,便可以实现ServerPush。

继续研究一下 ServerPush CDN的支持情况:在2016年4月28日,国外最大的提供商cloudflare宣布支持ServerPush。

而反观国内,各大CDN厂商仿佛对ServerPush这一HTTP/2特性熟视无睹。腾讯云、阿里云两大云厂商CDN产品文档都没有相关说明。只有一家小厂”又拍云“在2018年出过一篇PR文章:

让互联网更快,Server Push 特性及开启方式详解

经过重重查找,我们发现了一些端倪:

腾讯云+社区的一篇文章在开头指出,腾讯云已经支持ServerPush,并且进行了相关的性能测试。

万事俱备,只欠东风。然而,历史包袱是沉重的。静态资源和HTML不同域让CDN ServerPush成为了梦。

以常见的一个源站与CDN不同域的页面为例,HTML作为Web请求的入口,为了避免CDN Cache导致用户无法即时更新应用,一般选择不走CDN,直接解析到Origin的Nginx上。

而JS、CSS、IMG等静态资源,则是走CDN的域名。

HTML和CSS不在同一个域下,根本无法Push

访问一下国内的各大网站,发现基本上都是用了这种静态资源与HTML不同域的方案,要实现ServerPush,就要推动CDN主域化

CDN主域化是一个复杂、充满风险的工程,尤其是在业务高速运转时进行,无异于高速路上换轮胎。其方案要经过层层设计,本文在此就不做研究。

假设我们已经实现了CDN主域化,Server Push是不是可以马上提升性能呢?

未知不是武器:Push Cache

现阶段,Server无法得知Client是否有Cache

如果Client已经有该资源的Cache,那么Push的静态资源会毫无意义地浪费带宽。虽然浏览器可以通过RST_STREAM阻止Server继续向Client发送资源,但是一部分资源已经在网络中进行传输。

一些探索

Push CGI

饿了么的这篇文章:

浅谈HTTP/2 Server Push

给我们带来的比较好的启示,文章中提到, 为了避免静态资源和HTML不同域、Push Cache这两大问题,选择了不Push静态资源,而是Push CGI请求。

相较于Preload CGI逻辑,ServerPush CGI请求能够减少1个RTT时间,并且不用考虑Cache的问题,从而稳定地带来性能上的提升。

Cache Digests(draft)

目前,IETF已经有相关草案在进行讨论,使用Cache Digests,请求带上客户端的缓存情况供服务器识别,从而解决ServerPush的Push Cache问题。

103 Early Hints(draft)

目前 103 Early Hints 信息状态响应码也同样处于草案阶段,通过简单的HTTP回包来允许用户在服务器还在准备响应数据的时候预加载一些资源。

相较于Server Push,它避免了Push Cache的问题,但性能上没有Server Push极致。

性能数据

理论性能数据

下图总结了各种方案(Preload、103 Early Hints、Server Push、Server Push + Cache Digests)的性能:

(注:不考虑HTTPS TLS握手的RTT)

可以看到,Server Push + Cache Digests在性能上拥有很大的优势。希望它能尽快成为规范。

实测数据

Nginx Team对Server Push在实际场景下首次加载做了一次benchmark,如下所示:

原文:Introducing HTTP/2 Server Push with NGINX 1.13.9

Server Push在这种场景下也能带来一定性能提升。

When Push?

在什么情况下,我们应该使用Server Push?

RTT过长的情况

毫无疑问,当Client与Server的过长而带宽十分充足时,ServerPush节省的一个RTT能带来很好的优化。

一个新的问题是:RTT多长时,该使用ServerPush?

Google Chrome小组的一篇文章给出了计算公式:

Rules of Thumb for HTTP/2 Push

翻译如下:

遗憾的是,我们无法正常得知用户的带宽和RTT时间。

第一访问的用户

为了避免Push Cache这一问题,我们可以只为第一次访问的用户进行Push。

可以考虑使用Cookie来鉴别是否为第一次访问的用户,但也需要注意Cookie并不能完整的描述所有静态资源Cache的情况,举个例子:

Client Cookie没有失效,但是CSS资源的Cache已经失效,此时Server因为Client有Cookie而选择不进行Push。

当然,也可以设计更复杂的方案在Cookie中做标记,告诉Server Push的时机。

还好已经有方案来实现:

H2O 服务器提供了一个叫作 cache-aware server push的方案,原理就是将所有缓存过的资源都记录在 cookie 里,这样服务器就知道哪些资源不需要被推送了。

不过,在 cookie 里记录所有的资源路径会占用很多的空间,因此还需要将路径压缩一下。

这里可以考虑使用bloom filter来减少cookie数据量,可以点击查看这篇文章:

NGINX 支持 HTTP/2 server push 了

Client端渲染的情况

如果应用使用了SSR,Push静态资源可能会占用加载HTML的带宽,从而增加首屏时间。CSR的场景更适合Server Push。

在网络空闲时进行PUSH,只PUSH当前页面

由于Server Push可能会占据Client端本就窄小不充裕的带宽,在某些场景下,可能会起到适得其反的效果。

另一个原因是Server Push使用冷TCP连接,TCP的慢启动导致加载资源的效率比热TCP链接更慢

而下一个页面的资源由Service Worker来主动fetch相较来说要更为合适。


参考文章

Introducing HTTP/2 Server Push with NGINX 1.13.9

Rules of Thumb for HTTP/2 Push

浅谈 HTTP/2 Server Push

让互联网更快,Server Push 特性及开启方式详解

HTTP/2之服务器推送(Server Push)最佳实践

To push, or not to push?! - The future of HTTP/2 server push - Patrick Hamann - JSConf EU 2018