HTTP/2.0 Server Push 分析

1,617 阅读5分钟

Server Push,即服务器推送,rfc7540#section-8.2 中有详细的描述。简单来说,HTTP/2.0允许 Server 抢先发送/推送Response,以及相应的promise请求给 Client,并且与 Client 之前发起的请求相关联。

使用场景也有限制,前提是 Server 知道 Client 需要哪些资源,推送规则一般是提前配置好的。

No Server Push

HTTP/1.X里,没有Server Push,假如一个index.html页面包含三个资源:html代码css文件js文件。那么如果要完全加载这个页面,就需要三次Request-Response也就是3个RTT,流程如下:

Server Push 原理

我们先看看HTTP/2.0里的Server Push表现,还是刚才的例子,一个index.html页面包含三个资源:html代码css文件js文件,如果请求index.html(那么在命中Server Push的情况下),流程如下图:

对照上图,我们发现,同样是加载index.htmlServer Push流程里少了两个(style.cssmain.js)请求,也就减少了页面加载时间。

总结一下,Server Push的推送原理如下:

  1. Client 发起请求request

  2. 当 Client 的请求request 命中Server Push规则后,Server 首先会响应一个PUSH_PROMISE帧,也就是承诺会在一个新的Stream流上推送资源。

  3. Server 在当前请求的Stream流上响应当前请求。

  4. Server 在承诺的Stream流上推送具体的资源。

接下来我们看下Server Push里关键的两个frame帧。

PUSH_PROMISE

推送承诺帧(rfc7540#section-6.6)是Server Push的关键,目的是告诉 Client,即将在某个Stream上推送资源。Payload格式如下:

    +---------------+
    |Pad Length? (8)|
    +-+-------------+-----------------------------------------------+
    |R|                  Promised Stream ID (31)                    |
    +-+-----------------------------+-------------------------------+
    |                   Header Block Fragment (*)                 ...
    +---------------------------------------------------------------+
    |                           Padding (*)                       ...
    +---------------------------------------------------------------+

1. Pad Length

8位字段,包含帧的长度,以八位字节为单位的填充。仅当PADDED标志被设置。

2. R

单个保留位。

3. Promised Stream ID

无符号的31位整数,用于标识承诺的流Id

4. Header Block Fragment

标头块片段,包含请求标头字段,承诺推送的资源路径。

5. Padding

填充八位字节。

RST_STREAM

流重置帧(rfc7540#section-6.4)。如果 Client 收到了推送承诺PUSH_PROMISE帧,而且本地检测到了对应资源缓存,那么 Client 会发送RST_STREAMError Code = REFUSED_STREAM),拒绝该流,也就是告诉 Server,我不需要该资源了,别给我推送了。

RST_STREAM帧的Payload格式如下:

    +---------------------------------------------------------------+
    |                        Error Code (32)                        |
    +---------------------------------------------------------------+

1. REFUSED_STREAM:表示拒绝流。

2. CANCEL:表示取消流。

当然还有其他Code,具体可以看官方文档 rfc7540#section-6.4

推送竞争问题

Server Push一定能减少页面加载(请求响应)时间吗?答案是否定的。因为 Server 在响应页面的请求后,就立即开始在承诺的Stream流上推送了,尽管 Client 检测到本地有缓存,发送了RST_STREAM帧拒绝了该流的推送。但是由于时序问题,Server 在收到RST_STREAM之前,可能已经开始推送了,这样就造成了服务器带宽的浪费。就像下面这张图:

我们来做个测试,重复加载一个带Sever Push的页面,因为Chrome对资源有缓存机制,所以Client 会发送RST_STREAM帧。Wireshark实际数据包如下图:

如上图,Client 发送了RST_STREAM[6]RST_STREAM[8],但是 Server 已经开始在Stream6Stream8 上推送了部分数据,所以这种情况反而造成了带宽浪费。

推送测试

最后我们从0开始,来做一个测试,看看Server PushNo Server Push的各自表现,这里我们以静态资源为例。

1. 准备资源

首先在服务器上准备两个html页面:serverpush.htmlnoserverpush.html,内容一致如下:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
  <script src="/js/main.js"></script>
  <link rel="stylesheet" href="/css/style.css">
<title>Server Push</title>
</head>
<body>
    <p>hello server push</p>
</body>
</html>

接着准备/js/main.js/css/style.css文件。

2. 规则配置

接着配置Server Push规则,这里以nginx容器为例,修改nginx.conf,增加push规则:

location /serverpush.html {
      root /root/tomcat/html;
      index index.html index.htm;
      http2_push /css/style.css;
      http2_push /js/main.js;
}

保存重启nginx(nginx -s reload)。

3. 开始测试

接着我们请求 www.laoqingcai-已注销.com/serverpush.… ,通过Wireshark看到数据包交换流程如下:

如上图,整个推送流程也很简单:

  1. Client 请求/serverpush.html,打开Stream 1

  2. Server 在Stream 1上响应PUSH_PROMISE,承诺会在Stream 2上推送/css/style.css,在Stream 4上推送/js/main.js

  3. Server 在Stream 1上响应serverpush.html

  4. Server 在Stream 2上推送承诺的资源/css/style.css

  5. Server 在Stream 4上推送承诺的资源/js/main.js

整个过程也和我们分析的推送原理一致。针对 noserverpush 的抓包图这里就不贴了,有兴趣的可以自己用Wireshark抓包看下。

4. 结果对比

接着我们来看一下Server Push实际带来的好处。截了两张图,第一张是带Server Push的,第二张是No Server Push的。

经过多次测试对比(排除网络抖动的影响),我们发现,带Server Push的网页加载明显要比不带Server Push的要快20-30ms左右。 通过Chrome devtools可以看到,Server Push的资源加载过程少了几个阶段:

1. Stalled

连接的停滞阶段,即连接等待阶段,包含连接协商过程等等。

2. Request Sent

请求发送时间。

3. Waitting(TTFB)

请求发送后到收到 Server 响应的第1个Byte的耗时,受网络状况和 Server 处理能力影响。

其实归根结底,还是因为 Server Push 减少了页面内部的(资源)请求数量,从而减少了请求等待时间,以及请求往返次数(RTT),毕竟带宽不是瓶颈。

总结

最后我们总结一下,使用Server Push可以减少页面资源加载的RTT次数和加载的等待时间,最终减少页面的加载时间。特别是静态资源比较多的情况。

但是目前 Client 针对静态资源都有缓存机制,而且缓存的时间也比较长,如果使用Server Push,可能会出现推送竞争的情况,提前推送反而会浪费服务器带宽,所以在使用这个特性的时候需要仔细的考虑实际效果和带来的问题。

参考链接

rfc7540

饿了么 Server Push