深入研究:http2 的真正性能到底如何

zhangyanan 8年前
   <p style="text-align: center;"><img src="https://simg.open-open.com/show/b793cf8f4b59d32c8d774ee0d32160a9.jpg"></p>    <h2><strong>一、研究目的</strong></h2>    <p>http2的概念提出已经有相当长一段时间了,而网上关于关于http2的文章也一搜一大把。但是从搜索的结果来看,现有的文章多是偏向于对http2的介绍,鲜有真正从数据上具体分析的。这篇文章正是出于填补这块空缺内容的目的,通过一系列的实验以及数据分析,对http2的性能进行深入研究。当然,由于本人技术有限,实验所使用的方法肯定会有不足之处,如果各位看官有发现问题,还请向我提出,我一定会努力修改完善实验的方法的!</p>    <h2><strong>二、基础知识</strong></h2>    <p>通过学习相关资料,我们已经对HTTP2有了一个大致的认识,接下来将通过设计一个模型,对HTTP2的性能进行实验测试。</p>    <h2><strong>三、实验设计</strong></h2>    <p>设置实验组:搭建一个HTTP2(SPDY)服务器,能够以HTTP2的方式响应请求。同时,响应的内容大小,响应的延迟时间均可自定义。</p>    <p>设置对照组:搭建一个HTTP1.x服务器,以HTTP1.x的方式响应请求,其可自定义内容同实验组。另外为了减少误差,HTTP1.x服务器使用 https 协议。</p>    <p>测试过程:客户端通过设置响应的内容大小、请求资源的数量、延迟时间、上下行带宽等参数,分别对实验组服务器和对照组服务器发起请求,统计响应完成所需时间。</p>    <p>由于 nginx 切换成http2需要升级 nginx 版本以及取得https证书,且在服务器端的多种自定义设置所涉及的操作环节相对复杂,综合考虑之下放弃使用 nginx 作为实验用服务器的方案,而是采用了 NodeJS 方案。在实验的初始阶段,使用了原生的 NodeJS 搭配 node-http2 模块进行服务器搭建,后来改为了使用 express 框架搭配 node-spdy 模块搭建。原因是,原生 NodeJS 对于复杂请求的处理非常复杂, express 框架对请求、响应等已经做了一系列的优化,可以有效减少人为的误差。另外 node-http2 模块无法与 express 框架兼容,同时它的性能较之 node-spdy 模块也更低( <a href="/misc/goto?guid=4959721637142707056" rel="nofollow,noindex">General performance, node-spdy vs node-http2 #98</a> ),而 node-spdy 模块的功能与 node-http2 模块基本一致。</p>    <h3><strong>1、服务器搭建</strong></h3>    <p>实验组和对照组的服务器逻辑完全一致,关键代码如下:</p>    <pre>  app.get('/option/?', (req, res) => {      allow(res)      let size = req.query['size']      let delay = req.query['delay']      let buf = new Buffer(size * 1024 * 1024)      setTimeout(() => {          res.send(buf.toString('utf8'))      }, delay)  })</pre>    <p>其逻辑是,根据从客户端传入的参数,动态设置响应资源的大小和延迟时间。</p>    <h3><strong>2、客户端搭建</strong></h3>    <p>客户端可动态设置请求的次数、资源的数目、资源的大小和服务器延迟时间。同时搭配Chrome的开发者工具,可以人为模拟不同网络环境。在资源请求响应结束后,会自动计算总耗时时间。关键代码如下:</p>    <pre>  for (let i = 0; i < reqNum; i++) {      $.get(url, function (data) {          imageLoadTime(output, pageStart)      })  }</pre>    <p>客户端通过循环对资源进行多次请求,其数量可设置。每一次循环都会通过 imageLoadTime 更新时间,以实现时间统计的功能。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/f249a9492b683cdcc31ecbb9fd126887.gif"></p>    <h3><strong>3、实验项目</strong></h3>    <p>a. http2性能研究</p>    <p>通过研究章节二的文章内容,可以把http2的性能影响因素归结于“延迟”和“请求数目”。本实验增加了“资源体积”和“网络环境”作为影响因素,下面将会针对这四项进行详细的测试实验。其中每一次实验都会重复10次,取平均值后作记录。</p>    <p>b. 服务端推送研究</p>    <p>http2还有一项非常特别的功能——服务端推送。服务端推送允许服务器主动向客户端推送资源。本实验也会针对这个功能展开研究,主要研究服务端推送的使用方法及其对性能的影响。</p>    <h2><strong>四、http2性能数据统计</strong></h2>    <h3><strong>1、延迟因素对性能的影响</strong></h3>    <table>     <thead>      <tr>       <th>条件/实验次数</th>       <th>1</th>       <th>2</th>       <th>3</th>       <th>4</th>       <th>5</th>      </tr>     </thead>     <tbody>      <tr>       <td>延迟时间(ms)</td>       <td>0</td>       <td>10</td>       <td>20</td>       <td>30</td>       <td>40</td>      </tr>      <tr>       <td>资源数目(个)</td>       <td>100</td>       <td>100</td>       <td>100</td>       <td>100</td>       <td>100</td>      </tr>      <tr>       <td>资源大小(MB)</td>       <td>0.1</td>       <td>0.1</td>       <td>0.1</td>       <td>0.1</td>       <td>0.1</td>      </tr>      <tr>       <td>统计时间(s)http1.x</td>       <td>0.38</td>       <td>0.51</td>       <td>0.62</td>       <td>0.78</td>       <td>0.94</td>      </tr>      <tr>       <td>统计时间(s)http2</td>       <td>0.48</td>       <td>0.51</td>       <td>0.49</td>       <td>0.48</td>       <td>0.50</td>      </tr>     </tbody>    </table>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/6f79ef91b834e1cb9cbdccf9596b2bec.png"></p>    <h3><strong>2、请求数目对性能的影响</strong></h3>    <p>通过上一个实验,可以知道在延迟为10ms的时候,http1.x和http2的时间统计相近,故本次实验延迟时间设置为10ms。</p>    <table>     <thead>      <tr>       <th>条件/实验次数</th>       <th>1</th>       <th>2</th>       <th>3</th>       <th>4</th>       <th>5</th>      </tr>     </thead>     <tbody>      <tr>       <td>延迟时间(ms)</td>       <td>10</td>       <td>10</td>       <td>10</td>       <td>10</td>       <td>10</td>      </tr>      <tr>       <td>资源数目(个)</td>       <td>6</td>       <td>30</td>       <td>150</td>       <td>750</td>       <td>3750</td>      </tr>      <tr>       <td>资源大小(MB)</td>       <td>0.1</td>       <td>0.1</td>       <td>0.1</td>       <td>0.1</td>       <td>0.1</td>      </tr>      <tr>       <td>统计时间(s)http1.x</td>       <td>0.04</td>       <td>0.16</td>       <td>0.63</td>       <td>3.03</td>       <td>20.72</td>      </tr>      <tr>       <td>统计时间(s)http2</td>       <td>0.04</td>       <td>0.16</td>       <td>0.71</td>       <td>3.28</td>       <td>19.34</td>      </tr>     </tbody>    </table>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/07fa3522e3601bc7fef0c45fdd35173e.png"></p>    <p>增加延迟时间,重复实验:</p>    <table>     <thead>      <tr>       <th>条件/实验次数</th>       <th>6</th>       <th>7</th>       <th>8</th>       <th>9</th>       <th>10</th>      </tr>     </thead>     <tbody>      <tr>       <td>延迟时间(ms)</td>       <td>30</td>       <td>30</td>       <td>30</td>       <td>30</td>       <td>30</td>      </tr>      <tr>       <td>资源数目(个)</td>       <td>6</td>       <td>30</td>       <td>150</td>       <td>750</td>       <td>3750</td>      </tr>      <tr>       <td>资源大小(MB)</td>       <td>0.1</td>       <td>0.1</td>       <td>0.1</td>       <td>0.1</td>       <td>0.1</td>      </tr>      <tr>       <td>统计时间(s)http1.x</td>       <td>0.07</td>       <td>0.24</td>       <td>1.32</td>       <td>5.63</td>       <td>28.82</td>      </tr>      <tr>       <td>统计时间(s)http2</td>       <td>0.07</td>       <td>0.17</td>       <td>0.78</td>       <td>3.81</td>       <td>18.78</td>      </tr>     </tbody>    </table>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/84905c7fd2902a7847feb4321af90a88.png"></p>    <h3><strong>3、资源体积对性能的影响</strong></h3>    <p>通过上两个实验,可以知道在延迟为10ms,资源数目为30个的时候,http1.x和http2的时间统计相近,故本次实验延迟时间设置为10ms,资源数目30个。</p>    <table>     <thead>      <tr>       <th>条件/实验次数</th>       <th>1</th>       <th>2</th>       <th>3</th>       <th>4</th>       <th>5</th>      </tr>     </thead>     <tbody>      <tr>       <td>延迟时间(ms)</td>       <td>10</td>       <td>10</td>       <td>10</td>       <td>10</td>       <td>10</td>      </tr>      <tr>       <td>资源数目(个)</td>       <td>30</td>       <td>30</td>       <td>30</td>       <td>30</td>       <td>30</td>      </tr>      <tr>       <td>资源大小(MB)</td>       <td>0.2</td>       <td>0.4</td>       <td>0.6</td>       <td>0.8</td>       <td>1.0</td>      </tr>      <tr>       <td>统计时间(s)http1.x</td>       <td>0.21</td>       <td>0.37</td>       <td>0.59</td>       <td>0.68</td>       <td>0.68</td>      </tr>      <tr>       <td>统计时间(s)http2</td>       <td>0.25</td>       <td>0.45</td>       <td>0.61</td>       <td>0.83</td>       <td>0.73</td>      </tr>     </tbody>    </table>    <table>     <thead>      <tr>       <th>条件/实验次数</th>       <th>6</th>       <th>7</th>       <th>8</th>       <th>9</th>       <th>10</th>      </tr>     </thead>     <tbody>      <tr>       <td>延迟时间(ms)</td>       <td>10</td>       <td>10</td>       <td>10</td>       <td>10</td>       <td>10</td>      </tr>      <tr>       <td>资源数目(个)</td>       <td>30</td>       <td>30</td>       <td>30</td>       <td>30</td>       <td>30</td>      </tr>      <tr>       <td>资源大小(MB)</td>       <td>1.2</td>       <td>1.4</td>       <td>1.6</td>       <td>1.8</td>       <td>2.0</td>      </tr>      <tr>       <td>统计时间(s)http1.x</td>       <td>0.78</td>       <td>0.94</td>       <td>1.02</td>       <td>1.07</td>       <td>1.13</td>      </tr>      <tr>       <td>统计时间(s)http2</td>       <td>0.92</td>       <td>0.86</td>       <td>1.08</td>       <td>1.26</td>       <td>1.33</td>      </tr>     </tbody>    </table>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/fe67a078619cb55fab41654cff603515.png"></p>    <h3><strong>4、网络环境对性能的影响</strong></h3>    <p>通过上两个实验,可以知道在延迟为10ms,资源数目为30个的时候,http1.x和http2的时间统计相近,故本次实验延迟时间设置为10ms,资源数目30个。</p>    <table>     <thead>      <tr>       <th>条件/网络条件</th>       <th>Regular 2G</th>       <th>Good 2G</th>       <th>Regular 3G</th>       <th>Good 3G</th>       <th>Regular 4G</th>       <th>Wifi</th>      </tr>     </thead>     <tbody>      <tr>       <td>延迟时间(ms)</td>       <td>10</td>       <td>10</td>       <td>10</td>       <td>10</td>       <td>10</td>       <td>10</td>      </tr>      <tr>       <td>资源数目(个)</td>       <td>30</td>       <td>30</td>       <td>30</td>       <td>30</td>       <td>30</td>       <td>30</td>      </tr>      <tr>       <td>资源大小(MB)</td>       <td>0.1</td>       <td>0.1</td>       <td>0.1</td>       <td>0.1</td>       <td>0.1</td>       <td>0.1</td>      </tr>      <tr>       <td>统计时间(s)http1.x</td>       <td>222.66</td>       <td>116.64</td>       <td>67.37</td>       <td>32.82</td>       <td>11.89</td>       <td>0.87</td>      </tr>      <tr>       <td>统计时间(s)http2</td>       <td>138.06</td>       <td>71.02</td>       <td>40.77</td>       <td>20.82</td>       <td>7.70</td>       <td>0.94</td>      </tr>     </tbody>    </table>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/455a39f6c448c71a755ef9d7416ec2b4.png"></p>    <h2><strong>五、http2服务端推送实验</strong></h2>    <p>本实验主要针对网络环境对服务端推送速度的影响进行研究。在本实验中,所请求/推送的资源都是一个体积为290Kb的JS文件。每一个网络环境下都会重复十次实验,取平均值后填入表格。</p>    <table>     <thead>      <tr>       <th>条件/网络条件</th>       <th>Regular 2G</th>       <th>Good 2G</th>       <th>Regular 3G</th>       <th>Good 3G</th>       <th>Regular 4G</th>       <th>Wifi</th>      </tr>     </thead>     <tbody>      <tr>       <td>客户端请求总耗时(s)</td>       <td>9.59</td>       <td>5.30</td>       <td>3.21</td>       <td>1.57</td>       <td>0.63</td>       <td>0.12</td>      </tr>      <tr>       <td>服务端推送总耗时(s)</td>       <td>18.83</td>       <td>10.46</td>       <td>6.31</td>       <td>3.09</td>       <td>1.19</td>       <td>0.20</td>      </tr>      <tr>       <td>资源加载速度-客户端请求(s)</td>       <td>9.24</td>       <td>5.13</td>       <td>3.08</td>       <td>1.50</td>       <td>0.56</td>       <td>0.08</td>      </tr>      <tr>       <td>资源加载速度-服务端推送(s)</td>       <td>9.28</td>       <td>5.16</td>       <td>3.09</td>       <td>1.51</td>       <td>0.57</td>       <td>0.08</td>      </tr>     </tbody>    </table>    <table>     <thead>      <tr>       <th>条件/网络条件</th>       <th>No Throttling</th>      </tr>     </thead>     <tbody>      <tr>       <td>客户端请求总耗时(ms)</td>       <td>56</td>      </tr>      <tr>       <td>服务端推送总耗时(ms)</td>       <td>18</td>      </tr>      <tr>       <td>资源加载速度-客户端请求(s)</td>       <td>15.03</td>      </tr>      <tr>       <td>资源加载速度-服务端推送(s)</td>       <td>2.80</td>      </tr>     </tbody>    </table>    <p>从上述表格可以发现一个非常奇怪的现象,在开启了网络节流以后(包括Wifi选项),服务端推送的速度都远远比不上普通的客户端请求,但是在关闭了网络节流后,服务端推送的速度优势非常明显。在网络节流的Wifi选项中,下载速度为30M/s,上传速度为15M/s。而测试所用网络的实际下载速度却只有542K/s,上传速度只有142K/s,远远达不到网络节流Wifi选项的速度。为了分析这个原因,我们需要理解“服务端推送”的原理,以及推送过来的资源的存放位置在哪里。</p>    <p>普通的客户端请求过程如下图:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/472cab678b739b1c1937fc3e1e7bba9a.png"></p>    <p>服务端推送的过程如下图:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/f975578e73fdd4cd6ab4aea3e9a113a7.png"></p>    <p>从上述原理图可以知道,服务端推送能把客户端所需要的资源伴随着 index.html 一起发送到客户端,省去了客户端重复请求的步骤。正因为没有发起请求,建立连接等操作,所以静态资源通过服务端推送的方式可以极大地提升速度。但是这里又有一个问题,这些被推送的资源又是存放在哪里呢?参考了这篇文章 <a href="/misc/goto?guid=4959721637237834710" rel="nofollow,noindex">Issue 5: HTTP/2 Push</a> 以后,终于找到了原因。我们可以把服务端推送过程的原理图深入一下:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/c66ade377b919126c582004e23af005e.png"></p>    <p>服务端推送过来的资源,会统一放在一个网络与http缓存之间的一个地方,在这里可以理解为“本地”。当客户端把 index.html 解析完以后,会向 <strong>本地</strong> 请求这个资源。由于资源已经本地化,所以这个请求的速度非常快,这也是服务端推送性能优势的体现之一。当然,这个已经本地化的资源会返回200状态码,而非类似 localStorage 的304或者 200 (from cache) 状态码。Chrome的网络节流工具, 会在任何“网络请求”之间加入节流,由于服务端推送活来的静态资源也是返回200状态码,所以Chrome会把它当作网络请求来处理 ,于是导致了上述实验所看到的问题。</p>    <h2><strong>六、研究结论</strong></h2>    <p>通过上述一系列的实验,我们可以知道http2的性能优势集中体现在“多路复用”和“服务端推送”上。对于请求数目较少(约小于30个)的情况下,http1.x和http2的性能差异不大,在请求数目较多且延迟大于30ms的情况下,才能体现http2的性能优势。对于网络状况较差的环境,http2的性能也高于http1.x。与此同时,如果把静态资源都通过服务端推送的方式来处理,加载速度会得到更加巨大的提升。</p>    <p>在实际的应用中,由于http2多路复用的优势,前端应用团队无须采取把多个文件合并成一个,生成雪碧图之类的方法减少网络请求。除此之外,http2对于前端开发的影响并不大。</p>    <p>服务端升级http2,如果是使用 NodeJS 方案,只需要把 node-http 模块升级为 node-spdy 模块,并加入证书即可。</p>    <p>若要使用服务端推送,则在服务端需要对响应的逻辑进行扩展,这个需要视情况具体分析实施。</p>    <h2><strong>七、后记</strong></h2>    <p>纸上得来终觉浅,绝知此事要躬行。如果不是真正的设计实验、进行实验,我可能根本不会知道原来http2也有坑,原来使用Chrome做调试的时候也有需要注意的地方。</p>    <p>希望这篇文章能够对研究http2的同学有些许帮助吧,如文章开头所说,如果你发现我的实验设计有任何问题,或者你想到了更好的实验方式,也欢迎向我提出,我一定会认真研读你的建议的!</p>    <p>下面附送实验所需源码:</p>    <p>1、客户端页面</p>    <pre>  <!-- http1_vs_http2.html -->    <!DOCTYPE html>  <html lang="en">  <head>     <meta charset="UTF-8">     <title>http1 vs http2</title>     <script src="//cdn.bootcss.com/jquery/1.9.1/jquery.min.js"></script>     <style>         .box {             float: left;             width: 200px;             margin-right: 100px;             margin-bottom: 50px;             padding: 20px;             border: 4px solid pink;             font-family: Microsoft Yahei;         }         .box h2 {             margin: 5px 0;         }         .box .done {             color: pink;             font-weight: bold;             font-size: 18px;         }         .box button {             padding: 10px;             display: block;             margin: 10px 0;         }     </style>  </head>  <body>     <div class="box">         <h2>Http1.x</h2>         <p>Time: <span id="output-http1"></span></p>         <p class="done done-1">× Unfinished...</p>         <button class="btn-1">Get Response</button>     </div>       <div class="box">         <h2>Http2</h2>         <p>Time: <span id="output-http2"></span></p>         <p class="done done-1">× Unfinished...</p>         <button class="btn-2">Get Response</button>     </div>       <div class="box">         <h2>Options</h2>         <p>Request Num: <input type="text" id="req-num"></p>         <p>Request Size (Mb): <input type="text" id="req-size"></p>         <p>Request Delay (ms): <input type="text" id="req-delay"></p>     </div>       <script>         function imageLoadTime(id, pageStart) {           let lapsed = Date.now() - pageStart;           document.getElementById(id).innerHTML = ((lapsed) / 1000).toFixed(2) + 's'         }                  let boxes = document.querySelectorAll('.box')         let doneTip = document.querySelectorAll('.done')         let reqNumInput = document.querySelector('#req-num')         let reqSizeInput = document.querySelector('#req-size')         let reqDelayInput = document.querySelector('#req-delay')           let reqNum = 100         let reqSize = 0.1         let reqDelay = 300           reqNumInput.value = reqNum         reqSizeInput.value = reqSize         reqDelayInput.value = reqDelay           reqNumInput.onblur = function () {             reqNum = reqNumInput.value         }           reqSizeInput.onblur = function () {             reqSize = reqSizeInput.value         }           reqDelayInput.onblur = function () {             reqDelay = reqDelayInput.value         }           function clickEvents(index, url, output, server) {             doneTip[index].innerHTML = '× Unfinished...'             doneTip[index].style.color = 'pink'             boxes[index].style.borderColor = 'pink'             let pageStart = Date.now()             for (let i = 0; i < reqNum; i++) {                 $.get(url, function (data) {                     console.log(server + ' data')                     imageLoadTime(output, pageStart)                     if (i === reqNum - 1) {                         doneTip[index].innerHTML = '√ Finished!'                         doneTip[index].style.color = 'lightgreen'                         boxes[index].style.borderColor = 'lightgreen'                     }                 })             }         }           document.querySelector('.btn-1').onclick = function () {             clickEvents(0, 'https://localhost:1001/option?size=' + reqSize + '&delay=' + reqDelay, 'output-http1', 'http1.x')         }           document.querySelector('.btn-2').onclick = function () {             clickEvents(1, 'https://localhost:1002/option?size=' + reqSize + '&delay=' + reqDelay, 'output-http2', 'http2')         }     </script>  </body>  </html></pre>    <p>2、服务端代码(http1.x与http2仅有一处不同)</p>    <pre>  const http = require('https') // 若为http2则把'https'模块改为'spdy'模块  const url = require('url')  const fs = require('fs')  const express = require('express')  const path = require('path')    const app = express()    const options = {    key: fs.readFileSync(`${__dirname}/server.key`),    cert: fs.readFileSync(`${__dirname}/server.crt`)  }    const allow = (res) => {    res.header("Access-Control-Allow-Origin", "*")    res.header("Access-Control-Allow-Headers", "X-Requested-With")    res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS")  }    app.set('views', path.join(__dirname, 'views'))  app.set('view engine', 'ejs')  app.use(express.static(path.join(__dirname, 'static')))    app.get('/option/?', (req, res) => {      allow(res)      let size = req.query['size']      let delay = req.query['delay']      let buf = new Buffer(size * 1024 * 1024)      setTimeout(() => {          res.send(buf.toString('utf8'))      }, delay)  })    http.createServer(options, app).listen(1001, (err) => { // http2服务器端口为1002      if (err) throw new Error(err)      console.log('Http1.x server listening on port 1001.')  })</pre>    <p> </p>    <p>来自:https://segmentfault.com/a/1190000007219256</p>    <p> </p>