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

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


一、研究目的

http2的概念提出已经有相当长一段时间了,而网上关于关于http2的文章也一搜一大把。但是从搜索的结果来看,现有的文章多是偏向于对http2的介绍,鲜有真正从数据上具体分析的。这篇文章正是出于填补这块空缺内容的目的,通过一系列的实验以及数据分析,对http2的性能进行深入研究。当然,由于本人技术有限,实验所使用的方法肯定会有不足之处,如果各位看官有发现问题,还请向我提出,我一定会努力修改完善实验的方法的!

二、基础知识

关于HTTP2的基础知识,可以参考下列几篇文章,在这里就不进行赘述了。

通过学习相关资料,我们已经对HTTP2有了一个大致的认识,接下来将通过设计一个模型,对HTTP2的性能进行实验测试。

三、实验设计

设置实验组:搭建一个HTTP2(SPDY)服务器,能够以HTTP2的方式响应请求。同时,响应的内容大小,响应的延迟时间均可自定义。

设置对照组:搭建一个HTTP1.x服务器,以HTTP1.x的方式响应请求,其可自定义内容同实验组。另外为了减少误差,HTTP1.x服务器使用https协议。

测试过程:客户端通过设置响应的内容大小、请求资源的数量、延迟时间、上下行带宽等参数,分别对实验组服务器和对照组服务器发起请求,统计响应完成所需时间。

由于nginx切换成http2需要升级nginx版本以及取得https证书,且在服务器端的多种自定义设置所涉及的操作环节相对复杂,综合考虑之下放弃使用nginx作为实验用服务器的方案,而是采用了NodeJS方案。在实验的初始阶段,使用了原生的NodeJS搭配node-http2模块进行服务器搭建,后来改为了使用express框架搭配node-spdy模块搭建。原因是,原生NodeJS对于复杂请求的处理非常复杂,express框架对请求、响应等已经做了一系列的优化,可以有效减少人为的误差。另外node-http2模块无法与express框架兼容,同时它的性能较之node-spdy模块也更低(General performance, node-spdy vs node-http2 #98),而node-spdy模块的功能与node-http2模块基本一致。

1、服务器搭建

实验组和对照组的服务器逻辑完全一致,关键代码如下:


  1. app.get('/option/?', (req, res) => { 
  2.     allow(res) 
  3.     let size = req.query['size'
  4.     let delay = req.query['delay'
  5.     let buf = new Buffer(size * 1024 * 1024) 
  6.     setTimeout(() => { 
  7.         res.send(buf.toString('utf8')) 
  8.     }, delay) 
  9. })  

其逻辑是,根据从客户端传入的参数,动态设置响应资源的大小和延迟时间。

2、客户端搭建

客户端可动态设置请求的次数、资源的数目、资源的大小和服务器延迟时间。同时搭配Chrome的开发者工具,可以人为模拟不同网络环境。在资源请求响应结束后,会自动计算总耗时时间。关键代码如下:


  1. for (let i = 0; i < reqNum; i++) { 
  2.     $.get(url, function (data) { 
  3.         imageLoadTime(output, pageStart) 
  4.     }) 
  5. }  

客户端通过循环对资源进行多次请求,其数量可设置。每一次循环都会通过imageLoadTime更新时间,以实现时间统计的功能。

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

3、实验项目

a. http2性能研究

通过研究章节二的文章内容,可以把http2的性能影响因素归结于“延迟”和“请求数目”。本实验增加了“资源体积”和“网络环境”作为影响因素,下面将会针对这四项进行详细的测试实验。其中每一次实验都会重复10次,取平均值后作记录。

b. 服务端推送研究

http2还有一项非常特别的功能——服务端推送。服务端推送允许服务器主动向客户端推送资源。本实验也会针对这个功能展开研究,主要研究服务端推送的使用方法及其对性能的影响。

四、http2性能数据统计

1、延迟因素对性能的影响

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

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

2、请求数目对性能的影响

通过上一个实验,可以知道在延迟为10ms的时候,http1.x和http2的时间统计相近,故本次实验延迟时间设置为10ms。

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

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

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

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

3、资源体积对性能的影响

通过上两个实验,可以知道在延迟为10ms,资源数目为30个的时候,http1.x和http2的时间统计相近,故本次实验延迟时间设置为10ms,资源数目30个。

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

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

4、网络环境对性能的影响

通过上两个实验,可以知道在延迟为10ms,资源数目为30个的时候,http1.x和http2的时间统计相近,故本次实验延迟时间设置为10ms,资源数目30个。

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

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

五、http2服务端推送实验

本实验主要针对网络环境对服务端推送速度的影响进行研究。在本实验中,所请求/推送的资源都是一个体积为290Kb的JS文件。每一个网络环境下都会重复十次实验,取平均值后填入表格。

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

从上述表格可以发现一个非常奇怪的现象,在开启了网络节流以后(包括Wifi选项),服务端推送的速度都远远比不上普通的客户端请求,但是在关闭了网络节流后,服务端推送的速度优势非常明显。在网络节流的Wifi选项中,下载速度为30M/s,上传速度为15M/s。而测试所用网络的实际下载速度却只有542K/s,上传速度只有142K/s,远远达不到网络节流Wifi选项的速度。为了分析这个原因,我们需要理解“服务端推送”的原理,以及推送过来的资源的存放位置在哪里。

普通的客户端请求过程如下图:

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

服务端推送的过程如下图:

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

从上述原理图可以知道,服务端推送能把客户端所需要的资源伴随着index.html一起发送到客户端,省去了客户端重复请求的步骤。正因为没有发起请求,建立连接等操作,所以静态资源通过服务端推送的方式可以极大地提升速度。但是这里又有一个问题,这些被推送的资源又是存放在哪里呢?参考了这篇文章Issue 5: HTTP/2 Push以后,终于找到了原因。我们可以把服务端推送过程的原理图深入一下:

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

服务端推送过来的资源,会统一放在一个网络与http缓存之间的一个地方,在这里可以理解为“本地”。当客户端把index.html解析完以后,会向本地请求这个资源。由于资源已经本地化,所以这个请求的速度非常快,这也是服务端推送性能优势的体现之一。当然,这个已经本地化的资源会返回200状态码,而非类似localStorage的304或者200 (from cache)状态码。Chrome的网络节流工具,会在任何“网络请求”之间加入节流,由于服务端推送活来的静态资源也是返回200状态码,所以Chrome会把它当作网络请求来处理,于是导致了上述实验所看到的问题。

六、研究结论

通过上述一系列的实验,我们可以知道http2的性能优势集中体现在“多路复用”和“服务端推送”上。对于请求数目较少(约小于30个)的情况下,http1.x和http2的性能差异不大,在请求数目较多且延迟大于30ms的情况下,才能体现http2的性能优势。对于网络状况较差的环境,http2的性能也高于http1.x。与此同时,如果把静态资源都通过服务端推送的方式来处理,加载速度会得到更加巨大的提升。

在实际的应用中,由于http2多路复用的优势,前端应用团队无须采取把多个文件合并成一个,生成雪碧图之类的方法减少网络请求。除此之外,http2对于前端开发的影响并不大。

服务端升级http2,如果是使用NodeJS方案,只需要把node-http模块升级为node-spdy模块,并加入证书即可。nginx方案的话可以参考这篇文章:Open Source NGINX 1.9.5 Released with HTTP/2 Support

若要使用服务端推送,则在服务端需要对响应的逻辑进行扩展,这个需要视情况具体分析实施。

七、后记

纸上得来终觉浅,绝知此事要躬行。如果不是真正的设计实验、进行实验,我可能根本不会知道原来http2也有坑,原来使用Chrome做调试的时候也有需要注意的地方。

希望这篇文章能够对研究http2的同学有些许帮助吧,如文章开头所说,如果你发现我的实验设计有任何问题,或者你想到了更好的实验方式,也欢迎向我提出,我一定会认真研读你的建议的!

下面附送实验所需源码:1、客户端页面


  1. <!-- http1_vs_http2.html --> 
  2.  
  3. <!DOCTYPE html> 
  4. <html lang="en"
  5. <head> 
  6.    <meta charset="UTF-8"
  7.    <title>http1 vs http2</title> 
  8.    <script src="//cdn.bootcss.com/jquery/1.9.1/jquery.min.js"></script> 
  9.    <style> 
  10.        .box { 
  11.            floatleft
  12.            width: 200px; 
  13.            margin-right: 100px; 
  14.            margin-bottom: 50px; 
  15.            padding: 20px; 
  16.            border: 4px solid pink; 
  17.            font-family: Microsoft Yahei; 
  18.        } 
  19.        .box h2 { 
  20.            margin: 5px 0; 
  21.        } 
  22.        .box .done { 
  23.            color: pink; 
  24.            font-weight: bold; 
  25.            font-size: 18px; 
  26.        } 
  27.        .box button { 
  28.            padding: 10px; 
  29.            display: block; 
  30.            margin: 10px 0; 
  31.        } 
  32.    </style> 
  33. </head> 
  34. <body> 
  35.    <div class="box"
  36.        <h2>Http1.x</h2> 
  37.        <p>Time: <span id="output-http1"></span></p> 
  38.        <p class="done done-1">× Unfinished...</p> 
  39.        <button class="btn-1">Get Response</button> 
  40.    </div> 
  41.  
  42.    <div class="box"
  43.        <h2>Http2</h2> 
  44.        <p>Time: <span id="output-http2"></span></p> 
  45.        <p class="done done-1">× Unfinished...</p> 
  46.        <button class="btn-2">Get Response</button> 
  47.    </div> 
  48.  
  49.    <div class="box"
  50.        <h2>Options</h2> 
  51.        <p>Request Num: <input type="text" id="req-num"></p> 
  52.        <p>Request Size (Mb): <input type="text" id="req-size"></p> 
  53.        <p>Request Delay (ms): <input type="text" id="req-delay"></p> 
  54.    </div> 
  55.  
  56.    <script> 
  57.        function imageLoadTime(id, pageStart) { 
  58.          let lapsed = Date.now() - pageStart; 
  59.          document.getElementById(id).innerHTML = ((lapsed) / 1000).toFixed(2) + 's' 
  60.        } 
  61.         
  62.        let boxes = document.querySelectorAll('.box'
  63.        let doneTip = document.querySelectorAll('.done'
  64.        let reqNumInput = document.querySelector('#req-num'
  65.        let reqSizeInput = document.querySelector('#req-size'
  66.        let reqDelayInput = document.querySelector('#req-delay'
  67.  
  68.        let reqNum = 100 
  69.        let reqSize = 0.1 
  70.        let reqDelay = 300 
  71.  
  72.        reqNumInput.value = reqNum 
  73.        reqSizeInput.value = reqSize 
  74.        reqDelayInput.value = reqDelay 
  75.  
  76.        reqNumInput.onblur = function () { 
  77.            reqNum = reqNumInput.value 
  78.        } 
  79.  
  80.        reqSizeInput.onblur = function () { 
  81.            reqSize = reqSizeInput.value 
  82.        } 
  83.  
  84.        reqDelayInput.onblur = function () { 
  85.            reqDelay = reqDelayInput.value 
  86.        } 
  87.  
  88.        function clickEvents(index, url, output, server) { 
  89.            doneTip[index].innerHTML = '× Unfinished...' 
  90.            doneTip[index].style.color = 'pink' 
  91.            boxes[index].style.borderColor = 'pink' 
  92.            let pageStart = Date.now() 
  93.            for (let i = 0; i < reqNum; i++) { 
  94.                $.get(url, function (data) { 
  95.                    console.log(server + ' data'
  96.                    imageLoadTime(output, pageStart) 
  97.                    if (i === reqNum - 1) { 
  98.                        doneTip[index].innerHTML = '√ Finished!' 
  99.                        doneTip[index].style.color = 'lightgreen' 
  100.                        boxes[index].style.borderColor = 'lightgreen' 
  101.                    } 
  102.                }) 
  103.            } 
  104.        } 
  105.  
  106.        document.querySelector('.btn-1').onclick = function () { 
  107.            clickEvents(0, 'https://localhost:1001/option?size=' + reqSize + '&delay=' + reqDelay, 'output-http1''http1.x'
  108.        } 
  109.  
  110.        document.querySelector('.btn-2').onclick = function () { 
  111.            clickEvents(1, 'https://localhost:1002/option?size=' + reqSize + '&delay=' + reqDelay, 'output-http2''http2'
  112.        } 
  113.    </script> 
  114. </body> 
  115. </html>  

2、服务端代码(http1.x与http2仅有一处不同)


  1. const http = require('https') // 若为http2则把'https'模块改为'spdy'模块 
  2. const url = require('url'
  3. const fs = require('fs'
  4. const express = require('express'
  5. const path = require('path'
  6.  
  7. const app = express() 
  8.  
  9. const options = { 
  10.   key: fs.readFileSync(`${__dirname}/server.key`), 
  11.   cert: fs.readFileSync(`${__dirname}/server.crt`) 
  12.  
  13. const allow = (res) => { 
  14.   res.header("Access-Control-Allow-Origin""*"
  15.   res.header("Access-Control-Allow-Headers""X-Requested-With"
  16.   res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS"
  17.  
  18. app.set('views', path.join(__dirname, 'views')) 
  19. app.set('view engine''ejs'
  20. app.use(express.static(path.join(__dirname, 'static'))) 
  21.  
  22. app.get('/option/?', (req, res) => { 
  23.     allow(res) 
  24.     let size = req.query['size'
  25.     let delay = req.query['delay'
  26.     let buf = new Buffer(size * 1024 * 1024) 
  27.     setTimeout(() => { 
  28.         res.send(buf.toString('utf8')) 
  29.     }, delay) 
  30. }) 
  31.  
  32. http.createServer(options, app).listen(1001, (err) => { // http2服务器端口为1002 
  33.     if (err) throw new Error(err) 
  34.     console.log('Http1.x server listening on port 1001.'
  35. })  



作者:jrainlau

来源:51CTO

上一篇:ajax处理前端 js 与后端 ModelAndView 数据绑定


下一篇:聊聊并发(七)——Java中的阻塞队列