深入研究:HTTP2 的真正效能到底如何

Jrain發表於2019-01-02

寫於 2016.10.19

深入研究: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、延遲因素對效能的影響

條件/實驗次數 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

深入研究:HTTP2 的真正效能到底如何

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

深入研究:HTTP2 的真正效能到底如何

增加延遲時間,重複實驗:

條件/實驗次數 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

深入研究:HTTP2 的真正效能到底如何

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

深入研究:HTTP2 的真正效能到底如何

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 的真正效能到底如何

五、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選項的速度。為了分析這個原因,我們需要理解“服務端推送”的原理,以及推送過來的資源的存放位置在哪裡。

普通的客戶端請求過程如下圖:

深入研究: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.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.')
})
複製程式碼

相關文章