對node工程進行壓力測試與性能分析

上週在系統上線前,爲了看下系統能承受多大的併發和併發下的負載狀況,進行了一輪壓測。在壓測過程當中,發現服務器的cpu飈的的很是高,而tps,接口耗時、服務可用等都是正常的,臥槽,這就奇了怪了,本身想了半天也沒想出爲啥,不得已求助了大佬,大佬說先查看 cpu processor what?這是啥??雖然聽不懂,但能夠查嘛╭(╯^╰)╮,可還沒等我查出來,大佬直接上手,一頓騷操做,便找出了緣由~ 這着實讓本身汗顏啊,內功遠遠不足啊,回來網上找了資料,惡補一把如何分析node工程中的性能問題javascript

在開發過程當中,由於過於只關注了業務邏輯的實現,一些可能出現性能的點被忽略掉,並且這些點只能在量稍微大些的併發場景下才會出現,忘了在哪看到一句話 可能會出問題的點,便必定會出問題 性能問題進行分析必不可少html

樣例項目

爲了便於演示,寫了個簡單的小例子java

// app.js
const crypto = require('crypto')
const Koa = require('koa')
const Router = require('koa-router');

const app = new Koa();
const router = new Router();

router.get('/crypto', async(ctx, next) => {
    const salt = crypto.randomBytes(128).toString('base64')
    const hash = crypto.pbkdf2Sync('crypto', salt, 10000, 64, 'sha512').toString('hex')

    ctx.body = { hash: hash }
    console.log(hash)

    ctx.status = 200
    next()
});

let reqNum = 0
router.get('/empty', async(ctx, next) => {

    ctx.body = { hash: 'empty' }
    reqNum++;

    ctx.status = 200
    next()
});

app.use(router.routes()).use(router.allowedMethods());
app.listen(3000, () => {
    console.log("listen 3000")
})
複製代碼

基於koa2,有兩個路由,一個/crypto,其中的業務邏輯是,使用crypto庫對字符串加密;一個是 /empty,沒有業務邏輯的接口,就是個空接口node

壓力測試

壓力測試工具市面上有不少種,就不一一列舉了,在社區看到有人推薦 autocannon ,就對這個工具作個介紹,官方的簡介是 fast HTTP/1.1 benchmarking tool written in Node.js ,使用node編寫的壓測工具,能比wrk生成更多負載。git

install

npm i autocannon -g npm i autocannon --savegithub

use

提供兩種使用方式web

  1. 命令行 autocannon -c 100 -d 5 -p 2 http://127.0.0.1:3000/test 簡單快速
  2. api調用 autocannon(opts[, cb]) 便於編寫腳本

關鍵參數有這麼幾個npm

  • -c/--connections NUM 併發鏈接的數量,默認10
  • -p/--pipelining NUM 每一個鏈接的流水線請求請求數。默認1
  • -d/--duration SEC 執行的時間,單位秒
  • -m/--method METHOD 請求類型 默認GET
  • -b/--body BODY 請求報文體

還有不少參數,你們能夠查看官網文檔。json

這個庫目前只能支持一個接口壓測,我寫了個腳本,能夠支持批量壓測和生成測試報告,具體代碼見文末。api

report

下圖是對 /empty 接口壓測 autocannon -c 100 -d 5 -p 1 http://127.0.0.1:3000/empty 結果以下

autocannon-empty

可看到,每秒有100個連接,每一個連接一個請求,持續5秒,一共產生 31k 次請求。 報告分三部分,第一行表示接口的延遲,第二行表示每秒的請求數(tps),第三行表示每秒返回的字節數。那麼,延遲越低,tps越高,就表示接口性能越好,由於empty 是個空接口,因此它的tps=6221還不錯,響應時間也很快,咱們換成 /crypto 接口在試試

autocannon-crypto

立馬看出差距了,這個接口tps只有77,接口耗時達到了1100ms,說明這個接口有很大的優化空間啊

生成性能文件與分析

經過壓測工具咱們找到了有問題的接口,那接下來,就要對接口進行剖析了,但是光看接口代碼,很差分析啊,畢竟沒有說服力,咱們就須要一份性能報告,用數聽說話,下面介紹這個兩個方法給你們

V8 Profiler

V8 官方已經爲你們考慮到這點了,提供了Profiler工具 使用方式也很快捷,步驟以下(以app.js爲例)

生成報告

在啓動命令中加上 --prof ,如 node --prof app.js ,在項目根目錄會生成isolate-xxxxxxx-v8.log格式的文件,用來記錄運行期間的調用棧和時間等信息,其中內容以下(文件較大,就截取最頂端一小截)

v8-version,6,1,534,47,0
shared-library,"C:\Program Files\nodejs\node.exe",0x7ff7505f0000,0x7ff751c0f000,0
shared-library,"C:\WINDOWS\SYSTEM32\ntdll.dll",0x7ff8718a0000,0x7ff871a61000,0
shared-library,"C:\WINDOWS\system32\KERNEL32.DLL",0x7ff870590000,0x7ff87063d000,0
shared-library,"C:\WINDOWS\system32\KERNELBASE.dll",0x7ff86e830000,0x7ff86ea18000,0
shared-library,"C:\WINDOWS\system32\WS2_32.dll",0x7ff86ee00000,0x7ff86ee6b000,0
複製代碼
分析報告
  1. 對剛剛生成的log文件分析,仍是使用官方提供的工具 node --prof-process isolate-xxxxxxxx-v8.log,生成結果以下(去掉無用的部分)
Statistical profiling result from isolate-00000209B99A60A0-v8.log, (17704 ticks, 8 unaccounted, 0 excluded).

 [Shared libraries]:
   ticks  total  nonlib   name
  13795   77.9%          C:\WINDOWS\SYSTEM32\ntdll.dll
  ...

 [JavaScript]:
   ticks  total  nonlib   name
     12    0.1%   11.3%  Builtin: CallFunction_ReceiverIsAny
     ...

 [C++]:
   ticks  total  nonlib   name

 [Summary]:
   ticks  total  nonlib   name
     94    0.5%   88.7%  JavaScript
      0    0.0%    0.0%  C++
      8    0.0%    7.5%  GC
  17598   99.4%          Shared libraries
      8    0.0%          Unaccounted

 [C++ entry points]:
   ticks    cpp   total   name

 [Bottom up (heavy) profile]:
  Note: percentage shows a share of a particular caller in the total
  amount of its parent calls.
  Callers occupying less than 1.0% are not shown.

   ticks parent  name
  13795   77.9%  C:\WINDOWS\SYSTEM32\ntdll.dll

   3795   21.4%  C:\Program Files\nodejs\node.exe
   3768   99.3%    C:\Program Files\nodejs\node.exe
   3287   87.2%      Function: ~pbkdf2 crypto.js:633:16
   3287  100.0%        Function: ~exports.pbkdf2Sync crypto.js:628:30
   3287  100.0%          Function: ~router.get D:\github\webapp\js\usen\app.js:8:23
   3287  100.0%            Function: ~dispatch D:\github\webapp\js\usen\node_modules\_koa-compose@3.2.1@koa-compose\index.js:37:23
    ...
複製代碼

報告包含六部分:Shared libraries、JavaScript、C++、Summary、C++ entry points 和 Bottom up (heavy) profile,[JavaScript] 部分列出了 JavaScript 代碼執行所佔用的 CPU ticks(CPU 時鐘週期),[C++] 部分列出了 C++ 代碼執行所佔用的 CPU ticks,[Summary] 列出了各個部分的佔比,[Bottom up] 列出了全部 CPU 佔用時間從大到小的函數及堆棧信息。

根據 3287 87.2% Function: ~pbkdf2 crypto.js:633:16 可看出這個函數消耗了 87.2% 的cpu

  1. 文件的方式不直觀,那咱們換個UI界面的,步驟以下
  • 先clone v8的倉庫下來 git clone https://github.com/v8/v8.git
  • 將日誌文件轉換成 json格式 node --prof-process --preprocess isolate-xxxxxxxxxx-v8.log > v8.json
  • 打開 v8/tools/profview/index.html 文件,是個靜態界面,在界面中心選擇剛生成的 v8.json文件,文件解析成功後,界面以下

v8-ui

具體的功能就不一一解釋啦,咱們逐層展開,尋找耗時的點,很快便找到耗cpu的地方,以下圖

v8-ui-report

node佔比是45%,其中 pbkdf2 crypto.js便佔用了92%

v8-profiler

除了官方提供以外,咱們還能夠選擇開源大佬的庫,v8-profiler ,這個庫的建立的時間比較早,6年前便建立了,最近一次更是在一年半前,社區評價仍是不錯的

生成報告

生成方式很簡單,不足的是,須要硬編碼在項目中,以下

profiler.startProfiling('', true);
setTimeout(function() {
  var profile = profiler.stopProfiling('');
  profile.export()
     .pipe(fs.createWriteStream(`cpuprofile-${Date.now()}.cpuprofile`))
     .on('finish', () => profile.delete())
}, 1000);
複製代碼
解析報告
  1. Chrome

咱們的大Chrome要出馬啦,在Chrome的控制檯,有一欄 JavaScript Profile 以下圖

chrom-cpu

點擊load,選擇剛剛生成的文件,解析後以下

chrom-report

逐層查看,便了然

  1. flamegraph-火焰圖

使用 flamegraph 生成酷炫的火焰圖,用在報告那是酷炫的一逼,官網圖以下

flameGraph

使用方式就不細說啦

  1. v8-analytics

這個是社區大佬們,寫的一個開源庫 v8-analytics,官方介紹以下

解析v8-profiler和heapdump等工具輸出的cpu & heap-memory日誌,能夠提供

  • v8引擎逆優化或者優化失敗的函數標紅展現以及優化失敗緣由展現
  • 函數執行時長超過預期標紅展現
  • 當前項目中可疑的內存泄漏點展現

對應的命令以下

va test bailout --only 這個命令能夠只把那些v8引擎逆優化的函數列出來展現。

va test timeout 200 --only 這個命令能夠只把那些執時長超過200ms的函數列出來展現。

va test leak 可疑展現出測試的heapsnapshot文件中可疑的內存泄漏點。

這個庫的好處是,省的咱們一個個去點開查找,這樣能夠更加便於咱們篩選問題啦~

批量壓力測試及生成報告

autocannon 只能運行一個接口,要想在測試下一個接口,就得修改代碼,好比想批量測試多個接口,就須要來回改代碼,操做就比較麻煩,因此我基於 autocannon 寫了個腳本,能夠逐一壓測定義好的接口,同時還能夠生成測試報告。

'use strict'

const autocannon = require('autocannon')
const reporter = require('autocannon-reporter')
const path = require('path')
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

/** * @description * 運行autocannon * @author lizc * @param {*} param */
function makeAutocannon(param) {
    autocannon(param).on('done', handleResults)
}

/** * @description * 處理接口 * @author lizc * @param {*} result */
function handleResults(result) {
    const reportOutputPath = path.join(`./${result.title}_report.html`)
    reporter.writeReport(reporter.buildReport(result), reportOutputPath, (err, res) => {
        if (err) console.err('Error writting report: ', err)
        else console.log('Report written to: ', reportOutputPath)
    })
}

// 請求參數
const autocannonParam = {
    url: 'http://127.0.0.1:6100/',
    connections: 100,
    duration: 10,
    headers: {
        type: 'application/x-www-form-urlencoded'
    }
}
// 請求報文參數
const requestsParam = {
    method: 'POST', // this should be a put for modifying secret details
    headers: { // let submit some json?
        'Content-type': 'application/json; charset=utf-8'
    }
}

/** * @description * 啓動批量壓測 * @author lizc * @param {*} methodList 接口列表 */
async function run(methodList) {
    const autocannonList = methodList.map(val => {
        return {
            ...autocannonParam,
            url: autocannonParam.url + val,
            title: val,
            requests: [
                {
                    ...requestsParam,
                }
            ],
        }
    })
    for (let i = 0; i < autocannonList.length; i++) {
        if (i !== 0) {
            await sleep((autocannonList[i - 1].duration + 2) * 1000)
            makeAutocannon(autocannonList[i])
        } else {
            makeAutocannon(autocannonList[i])
        }
    }
}
// 啓動
run(['order', 'crypto'])
複製代碼

小結

我是github的搬運工

以上的方法基本上能知足咱們的需求,固然性能涉及的方方面面不少好比內存泄漏、事物等,性能調優路漫漫呀, 文章大部分東西都是來自大佬們的總結,我只是在作一次整理彙總,便於本身理解與查閱,但願能幫到小夥伴們~

參考連接

github.com/nswbmw/node…

github.com/hyj1991/v8-…

cnodejs.org/topic/58b56…

github.com/mcollina/au…

www.helplib.com/GitHub/arti…

github.com/nearform/no…

相關文章
相關標籤/搜索