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

zhangyanan 8年前發布 | 14K 次閱讀 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 更新時間,以實現時間統計的功能。

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 模塊,并加入證書即可。

若要使用服務端推送,則在服務端需要對響應的邏輯進行擴展,這個需要視情況具體分析實施。

七、后記

紙上得來終覺淺,絕知此事要躬行。如果不是真正的設計實驗、進行實驗,我可能根本不會知道原來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="http://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.')
})

 

來自:https://segmentfault.com/a/1190000007219256

 

 本文由用戶 zhangyanan 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!