深入研究: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、服务器搭建

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

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

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

2、客户端搭建

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

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

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

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

3、实验项目

a. http2 性能研究

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

b. 服务端推送研究

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

四、http2 性能数据统计

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

条件 / 实验次数12345
延迟时间(ms)010203040
资源数目(个)100100100100100
资源大小(MB)0.10.10.10.10.1
统计时间(s)http1.x0.380.510.620.780.94
统计时间(s)http20.480.510.490.480.50

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

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

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

条件 / 实验次数12345
延迟时间(ms)1010101010
资源数目(个)6301507503750
资源大小(MB)0.10.10.10.10.1
统计时间(s)http1.x0.040.160.633.0320.72
统计时间(s)http20.040.160.713.2819.34

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

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

条件 / 实验次数678910
延迟时间(ms)3030303030
资源数目(个)6301507503750
资源大小(MB)0.10.10.10.10.1
统计时间(s)http1.x0.070.241.325.6328.82
统计时间(s)http20.070.170.783.8118.78

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

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

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

条件 / 实验次数12345
延迟时间(ms)1010101010
资源数目(个)3030303030
资源大小(MB)0.20.40.60.81.0
统计时间(s)http1.x0.210.370.590.680.68
统计时间(s)http20.250.450.610.830.73
条件 / 实验次数678910
延迟时间(ms)1010101010
资源数目(个)3030303030
资源大小(MB)1.21.41.61.82.0
统计时间(s)http1.x0.780.941.021.071.13
统计时间(s)http20.920.861.081.261.33

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

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

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

条件 / 网络条件Regular 2GGood 2GRegular 3GGood 3GRegular 4GWifi
延迟时间(ms)101010101010
资源数目(个)303030303030
资源大小(MB)0.10.10.10.10.10.1
统计时间(s)http1.x222.66116.6467.3732.8211.890.87
统计时间(s)http2138.0671.0240.7720.827.700.94

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

五、http2 服务端推送实验

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

条件 / 网络条件Regular 2GGood 2GRegular 3GGood 3GRegular 4GWifi
客户端请求总耗时(s)9.595.303.211.570.630.12
服务端推送总耗时(s)18.8310.466.313.091.190.20
资源加载速度-客户端请求(s)9.245.133.081.500.560.08
资源加载速度-服务端推送(s)9.285.163.091.510.570.08
条件 / 网络条件No Throttling
客户端请求总耗时(ms)56
服务端推送总耗时(ms)18
资源加载速度-客户端请求(s)15.03
资源加载速度-服务端推送(s)2.80

从上述表格可以发现一个非常奇怪的现象,在开启了网络节流以后(包括 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、客户端页面


   




   
   http1 vs http2
   
   
       .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;
       }
   


   
   
       

Http1.x

Time:

× Unfinished...

Get Response

Http2

Time:

× Unfinished...

Get Response

Options

Request Num:

Request Size (Mb):

Request Delay (ms):

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

2、服务端代码(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.')
})
    原文作者:HTTP
    原文地址: https://juejin.im/entry/58076f83bf22ec0064fa630b
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞