深刻研究:HTTP2 的真正性能到底如何

图片描述

1、研究目的

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

2、基础知识

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

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

3、实验设计

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

设置对照组:搭建一个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模块基本一致。

一、服务器搭建

实验组和对照组的服务器逻辑彻底一致,关键代码以下:

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)
})

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

二、客户端搭建

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

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

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

图片描述

三、实验项目

a. http2性能研究

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

b. 服务端推送研究

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

4、HTTP2 性能数据统计

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

条件/实验次数 1 2 3 4 5
延迟时间(ms) 0 10 20 30 40
资源数目(个) 100 100 100 100 100
资源大小(MB) 0.1 0.1 0.1 0.1 0.1
统计时间(s)http1.x 0.38 0.51 0.62 0.78 0.94
统计时间(s)http2 0.48 0.51 0.49 0.48 0.50

延迟因素对性能的影响

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

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

条件/实验次数 1 2 3 4 5
延迟时间(ms) 10 10 10 10 10
资源数目(个) 6 30 150 750 3750
资源大小(MB) 0.1 0.1 0.1 0.1 0.1
统计时间(s)http1.x 0.04 0.16 0.63 3.03 20.72
统计时间(s)http2 0.04 0.16 0.71 3.28 19.34

请求数目对性能的影响

增长延迟时间,重复实验:

条件/实验次数 6 7 8 9 10
延迟时间(ms) 30 30 30 30 30
资源数目(个) 6 30 150 750 3750
资源大小(MB) 0.1 0.1 0.1 0.1 0.1
统计时间(s)http1.x 0.07 0.24 1.32 5.63 28.82
统计时间(s)http2 0.07 0.17 0.78 3.81 18.78

请求数目对性能的影响

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

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

条件/实验次数 1 2 3 4 5
延迟时间(ms) 10 10 10 10 10
资源数目(个) 30 30 30 30 30
资源大小(MB) 0.2 0.4 0.6 0.8 1.0
统计时间(s)http1.x 0.21 0.37 0.59 0.68 0.68
统计时间(s)http2 0.25 0.45 0.61 0.83 0.73
条件/实验次数 6 7 8 9 10
延迟时间(ms) 10 10 10 10 10
资源数目(个) 30 30 30 30 30
资源大小(MB) 1.2 1.4 1.6 1.8 2.0
统计时间(s)http1.x 0.78 0.94 1.02 1.07 1.13
统计时间(s)http2 0.92 0.86 1.08 1.26 1.33

图片描述

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

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

条件/网络条件 Regular 2G Good 2G Regular 3G Good 3G Regular 4G Wifi
延迟时间(ms) 10 10 10 10 10 10
资源数目(个) 30 30 30 30 30 30
资源大小(MB) 0.1 0.1 0.1 0.1 0.1 0.1
统计时间(s)http1.x 222.66 116.64 67.37 32.82 11.89 0.87
统计时间(s)http2 138.06 71.02 40.77 20.82 7.70 0.94

图片描述

5、HTTP2 服务端推送实验

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

条件/网络条件 Regular 2G Good 2G Regular 3G Good 3G Regular 4G Wifi
客户端请求总耗时(s) 9.59 5.30 3.21 1.57 0.63 0.12
服务端推送总耗时(s) 18.83 10.46 6.31 3.09 1.19 0.20
资源加载速度-客户端请求(s) 9.24 5.13 3.08 1.50 0.56 0.08
资源加载速度-服务端推送(s) 9.28 5.16 3.09 1.51 0.57 0.08
条件/网络条件 No Throttling
客户端请求总耗时(ms) 56
服务端推送总耗时(ms) 18
资源加载速度-客户端请求(s) 15.03
资源加载速度-服务端推送(s) 2.80

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

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

图片描述

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

图片描述

从上述原理图能够知道,服务端推送能把客户端所须要的资源伴随着index.html一块儿发送到客户端,省去了客户端重复请求的步骤。正由于没有发起请求,创建链接等操做,因此静态资源经过服务端推送的方式能够极大地提高速度。可是这里又有一个问题,这些被推送的资源又是存放在哪里呢?参考了这篇文章Issue 5: HTTP/2 Push之后,终于找到了缘由。咱们能够把服务端推送过程的原理图深刻一下:

图片描述

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

6、研究结论

经过上述一系列的实验,咱们能够知道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

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

7、后记

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

但愿这篇文章可以对研究http2的同窗有些许帮助吧,如文章开头所说,若是你发现个人实验设计有任何问题,或者你想到了更好的实验方式,也欢迎向我提出,我必定会认真研读你的建议的!


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

<!-- 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>

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

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.')
})
相关文章
相关标签/搜索