寫於 2016.10.19
一、研究目的
HTTP2的概念提出已經有相當長一段時間了,而網上關於關於http2的文章也一搜一大把。但是從搜尋的結果來看,現有的文章多是偏向於對http2的介紹,鮮有真正從資料上具體分析的。這篇文章正是出於填補這塊空缺內容的目的,通過一系列的實驗以及資料分析,對http2的效能進行深入研究。當然,由於本人技術有限,實驗所使用的方法肯定會有不足之處,如果各位看官有發現問題,還請向我提出,我一定會努力修改完善實驗的方法的!
二、基礎知識
關於HTTP2的基礎知識,可以參考下列幾篇文章,在這裡就不進行贅述了。
- HTTP,HTTP2.0,SPDY,HTTPS你應該知道的一些事,
- HTTP 2.0的那些事
- HTTP2.0的奇妙日常
- 一分鐘預覽 HTTP2 特性和抓包分析
- HTTP/2 for web application developers
- 7 Tips for Faster HTTP/2 Performance
通過學習相關資料,我們已經對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
更新時間,以實現時間統計的功能。
3、實驗專案
a. http2效能研究
通過研究章節二的文章內容,可以把http2的效能影響因素歸結於“延遲”和“請求數目”。本實驗增加了“資源體積”和“網路環境”作為影響因素,下面將會針對這四項進行詳細的測試實驗。其中每一次實驗都會重複10次,取平均值後作記錄。
b. 服務端推送研究
http2還有一項非常特別的功能——服務端推送。服務端推送允許伺服器主動向客戶端推送資源。本實驗也會針對這個功能展開研究,主要研究服務端推送的使用方法及其對效能的影響。
四、HTTP2 效能資料統計
1、延遲因素對效能的影響
條件/實驗次數 | 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 |
2、請求數目對效能的影響
通過上一個實驗,可以知道在延遲為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 |
3、資源體積對效能的影響
通過上兩個實驗,可以知道在延遲為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 |
4、網路環境對效能的影響
通過上兩個實驗,可以知道在延遲為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 |
五、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會把它當作網路請求來處理,於是導致了上述實驗所看到的問題。
六、研究結論
通過上述一系列的實驗,我們可以知道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.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>
複製程式碼
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.')
})
複製程式碼