自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

保持Node.js的速度-創(chuàng)建高性能Node.js Servers的工具、技術(shù)和提示

開發(fā) 前端
Node 是一個(gè)非常多彩的平臺(tái),而創(chuàng)建network服務(wù)就是其非常重要的能力之一。在本文我們將關(guān)注最主流的: HTTP Web servers.

快速摘要

Node 是一個(gè)非常多彩的平臺(tái),而創(chuàng)建network服務(wù)就是其非常重要的能力之一。在本文我們將關(guān)注最主流的: HTTP Web servers.

引子

如果你已經(jīng)使用Node.js足夠長(zhǎng)的時(shí)間,那么毫無疑問你會(huì)碰到比較痛苦的速度問題。JavaScript是一種事件驅(qū)動(dòng)的、異步的語言。這很明顯使得對(duì)性能的推理變得棘手。Node.js的迅速普及使得我們必須尋找適合這種server-side javacscript的工具、技術(shù)。

當(dāng)我們碰到性能問題,在瀏覽器端的經(jīng)驗(yàn)將無法適用于服務(wù)器端。所以我們?nèi)绾未_保一個(gè)Node.js代碼是快速的且能達(dá)到我們的要求呢?讓我們來動(dòng)手看一些實(shí)例

工具

我們需要一個(gè)工具來壓測(cè)我們的server從而測(cè)量性能。比如,我們使用 autocannon

 

  1. npm install -g autocannon // 或使用淘寶源cnpm, 騰訊源tnpm 

其他的Http benchmarking tools 包括 Apache Bench(ab) 和 wrk2, 但AutoCannon是用Node寫的,對(duì)前端來說會(huì)更加方便并易于安裝,它可以非常方便的安裝在 Windows、Linux 和Mac OS X.

當(dāng)我們安裝了基準(zhǔn)性能測(cè)試工具,我們需要用一些方法去診斷我們的程序。一個(gè)很不錯(cuò)的診斷性能問題的工具便是 Node Clinic 。它也可以用npm安裝:

 

  1. npm install -g clinic 

這實(shí)際上會(huì)安裝一系列套件,我們將使用 Clinic Doctor

和 Clinic Flame (一個(gè) ox 的封裝)

譯者注: ox是一個(gè)自動(dòng)剖析cpu并生成node進(jìn)程火焰圖的工具; 而clinic Flame就是基于ox的封裝。

另一方面, clinic工具本身其實(shí)是一系列套件的組合,它不同的子命令分別會(huì)調(diào)用到不同的子模塊,例如:

  • 醫(yī)生診斷功能。The doctor functionality is provided by Clinic.js Doctor.
  • 氣泡診斷功能。The bubbleprof functionality is provided by Clinic.js Bubbleprof.
  • 火焰圖功能。 The flame functionality is provided by Clinic.js Flame.)

tips: 對(duì)于本文實(shí)例,需要 Node 8.11.2 或更高版本

代碼示例

我們的例子是一個(gè)只有一個(gè)資源的簡(jiǎn)單的 REST server:暴露一個(gè) GET 訪問的路由 /seed/v1 ,返回一個(gè)大 JSON 載荷。 server端的代碼就是一個(gè)app目錄,里面包括一個(gè) packkage.json (依賴 restify 7.1.0)、一個(gè) index.js 和 一個(gè) util.js (譯者注: 放一些工具函數(shù))

 

  1. // index.js  
  2. const restify = require('restify')  
  3. const server = restify.createServer()  
  4. const { etagger, timestamp, fetchContent } from './util'  
  5. server.use(etagger.bind(server)) // 綁定etagger中間件,可以給資源請(qǐng)求加上etag響應(yīng)頭  
  6. server.get('/seed/v1', function () {  
  7.   fetchContent(req.url, (err, content) => {  
  8.     if (err) {  
  9.       return next(err)  
  10.     }  
  11.     res.send({data: content, ts: timestamp(), url: req.url})  
  12.     next()  
  13.   })  
  14. })  
  15. server.listen(8080, function () {  
  16.   cosnole.log(' %s listening at %s',  server.name, server.url)  
  17. }) 

 

 

  1. // util.js  
  2. const restify = require('restify')  
  3. const crypto = require('crypto')  
  4. module.exports = function () {  
  5.     const content = crypto.rng('5000').toString('hex') // 普通有規(guī)則的隨機(jī)  
  6.     const fetchContent = function (url, cb) {  
  7.         setImmediate(function () {  
  8.         if (url !== '/seed/v1') return restify.errors.NotFoundError('no api!')  
  9.             cb(content)  
  10.         })  
  11.     }   
  12.     let last = Date.now()  
  13.     const TIME_ONE_MINUTE = 60000  
  14.     const timestamp = function () {  
  15.       const now = Date.now()  
  16.       if (now - last >= TIME_ONE_MINITE) {  
  17.           last = now  
  18.       }  
  19.       return last  
  20.     }    
  21.     const etagger = function () {  
  22.         const cache = {}  
  23.         let afterEventAttached  = false  
  24.         function attachAfterEvent(server) {  
  25.             if (attachAfterEvent ) return  
  26.             afterEventAttached  = true  
  27.             server.on('after', function (req, res) {  
  28.                 if (res.statusCode == 200 && res._body != null) {  
  29.                     const urlKey = crpto.createHash('sha512')  
  30.                         .update(req.url)  
  31.                         .digets()  
  32.                         .toString('hex')  
  33.                     const contentHash = crypto.createHash('sha512')  
  34.                     .update(JSON.stringify(res._body))  
  35.                     .digest()  
  36.                     .toString('hex')  
  37.                     if (cache[urlKey] != contentHash) cache[urlKey] = contentHash  
  38.                 }  
  39.             })  
  40.         }  
  41.          return function(req, res, next) {  
  42.                 // 譯者注: 這里attachEvent的位置好像不太優(yōu)雅,我換另一種方式改了下這里??梢詤⒖? https://github.com/cuiyongjian/study-restify/tree/master/app  
  43.                 attachAfterEvent(this) // 給server注冊(cè)一個(gè)after鉤子,每次即將響應(yīng)數(shù)據(jù)時(shí)去計(jì)算body的etag值  
  44.             const urlKey = crypto.createHash('sha512')  
  45.             .update(req.url)  
  46.             .digest()  
  47.             .toString('hex')  
  48.             // 譯者注: 這里etag的返回邏輯應(yīng)該有點(diǎn)小問題,每次請(qǐng)求都是返回的上次寫入cache的etag  
  49.             if (urlKey in cache) res.set('Etag', cache[urlKey])  
  50.             res.set('Cache-Control', 'public; max-age=120')  
  51.         }  
  52.     }     
  53.     return { fetchContent, timestamp, etagger }  

 

務(wù)必不要用這段代碼作為***實(shí)踐,因?yàn)檫@里面有很多代碼的壞味道,但是我們接下來將測(cè)量并找出這些問題。

要獲得這個(gè)例子的源碼可以去這里

Profiling 剖析

為了剖析我們的代碼,我們需要兩個(gè)終端窗口。一個(gè)用來啟動(dòng)app,另外一個(gè)用來壓測(cè)他。

***個(gè)terminal,我們執(zhí)行:

 

  1. node ./index.js 

另外一個(gè)terminal,我們這樣剖析他(譯者注: 實(shí)際是在壓測(cè)):

 

  1. autocannon -c100 localhost:3000/seed/v1 

這將打開100個(gè)并發(fā)請(qǐng)求轟炸服務(wù),持續(xù)10秒。

結(jié)果大概是下面這個(gè)樣子:

stat avg stdev Max
耗時(shí)(毫秒) 3086.81 1725.2 5554
吞吐量(請(qǐng)求/秒) 23.1 19.18 65
每秒傳輸量(字節(jié)/秒) 237.98 kB 197.7 kB 688.13 kB

231 requests in 10s, 2.4 MB read

結(jié)果會(huì)根據(jù)你機(jī)器情況變化。然而我們知道: 一般的“Hello World”Node.js服務(wù)器很容易在同樣的機(jī)器上每秒完成三萬個(gè)請(qǐng)求,現(xiàn)在這段代碼只能承受每秒23個(gè)請(qǐng)求且平均延遲超過3秒,這是令人沮喪的。

譯者注: 我用公司macpro18款 15寸 16G 256G,測(cè)試結(jié)果如下:

診斷

定位問題

我們可以通過一句命令來診斷應(yīng)用,感謝 clinic doctor 的 -on-port 命令。在app目錄下,我們執(zhí)行:

 

  1. clinic doctor --on-port='autocannon -c100 localhost:3000/seed/v1' -- node index.js 

譯者注:

現(xiàn)在autocannon的話可以使用新的subarg形式的命令語法:

 

  1. clinic doctor --autocannon [ /seed/v1 -c 100 ] -- node index.js 

clinic doctor會(huì)在剖析完畢后,創(chuàng)建html文件并自動(dòng)打開瀏覽器。

結(jié)果長(zhǎng)這個(gè)樣子:

譯者的測(cè)試長(zhǎng)這樣子:-

譯者注:橫坐標(biāo)其實(shí)是你系統(tǒng)時(shí)間,冒號(hào)后面的表示當(dāng)前的系統(tǒng)時(shí)間的 - 秒數(shù)。

備注:接下來的文章內(nèi)容分析,我們還是以原文的統(tǒng)計(jì)結(jié)果圖片為依據(jù)。

跟隨UI頂部的消息,我們看到 EventLoop 圖表,它的確是紅色的,并且這個(gè)EventLoop延遲在持續(xù)增長(zhǎng)。在我們深入研究他意味著什么之前,我們先了解下其他指標(biāo)下的診斷。

我們可以看到CPU一直在100%或超過100%這里徘徊,因?yàn)檫M(jìn)程正在努力處理排隊(duì)的請(qǐng)求。Node的 JavaScript 引擎(也就是V8) 著這里實(shí)際上用 2 個(gè) CPU核心在工作,因?yàn)闄C(jī)器是多核的 而V8會(huì)用2個(gè)線程。 一個(gè)線程用來執(zhí)行 EventLoop,另外一個(gè)線程用來垃圾收集。 當(dāng)CPU高達(dá)120%的時(shí)候就是進(jìn)程在回收處理完的請(qǐng)求的遺留對(duì)象了(譯者注: 操作系統(tǒng)的進(jìn)程CPU使用率的確經(jīng)常會(huì)超過100%,這是因?yàn)檫M(jìn)程內(nèi)用了多線程,OS把工作分配到了多個(gè)核心,因此統(tǒng)計(jì)cpu占用時(shí)間時(shí)會(huì)超過100%)

我們看與之相關(guān)的內(nèi)存圖表。實(shí)線表示內(nèi)存的堆內(nèi)存占用(譯者注:RSS表示node進(jìn)程實(shí)際占用的內(nèi)存,heapUsage堆內(nèi)存占用就是指的堆區(qū)域占用了多少,THA就表示總共申請(qǐng)到了多少堆內(nèi)存。一般看heapUsage就好,因?yàn)樗硎玖薾ode代碼中大多數(shù)JavaScript對(duì)象所占用的內(nèi)存)。我們看到,只要CPU圖表上升一下則堆內(nèi)存占用就下降一些,這表示內(nèi)存正在被回收。

activeHandler跟EventLoop的延遲沒有什么相關(guān)性。一個(gè)active hanlder 就是一個(gè)表達(dá) I/O的對(duì)象(比如socket或文件句柄) 或者一個(gè)timer (比如setInterval)。我們用autocannon創(chuàng)建了100連接的請(qǐng)求(-c100), activehandlers 保持在103. 額外的3個(gè)handler其實(shí)是 STDOUT,STDERROR 以及 server 對(duì)象自身(譯者: server自身也是個(gè)socket監(jiān)聽句柄)。

如果我們點(diǎn)擊一下UI界面上底部的建議pannel面板,我們會(huì)看到:

短期緩解

深入分析性能問題需要花費(fèi)大量的時(shí)間。在一個(gè)現(xiàn)網(wǎng)項(xiàng)目中,可以給服務(wù)器或服務(wù)添加過載保護(hù)。過載保護(hù)的思路就是檢測(cè) EventLoop 延遲(以及其他指標(biāo)),然后在超過閾值時(shí)響應(yīng)一個(gè) "503 Service Unavailable"。這就可以讓 負(fù)載均衡器轉(zhuǎn)向其他server實(shí)例,或者實(shí)在不行就讓用戶過一會(huì)重試。overload-protection-module 這個(gè)過載保護(hù)模塊能直接低成本地接入到 Express、Koa 和 Restify使用。Hapi 框架也有一個(gè)配置項(xiàng)提供同樣的過載保護(hù)。(譯者注:實(shí)際上看overload-protection模塊的底層就是通過loopbench 實(shí)現(xiàn)的EventLoop延遲采樣,而loopbench就是從Hapi框架里抽離出來的一個(gè)模塊;至于內(nèi)存占用,則是overload-protection內(nèi)部自己實(shí)現(xiàn)的采樣,畢竟直接用memoryUsage的api就好了)

理解問題所在

就像 Clinic Doctor 說的,如果 EventLoop 延遲到我們觀察的這個(gè)樣子,很可能有一個(gè)或多個(gè)函數(shù)阻塞了事件循環(huán)。認(rèn)識(shí)到Node.js的這個(gè)主要特性非常重要:在當(dāng)前的同步代碼執(zhí)行完成之前,異步事件是無法被執(zhí)行的。這就是為什么下面 setTimeout 不能按照預(yù)料的時(shí)間觸發(fā)的原因。

舉例,在瀏覽器開發(fā)者工具或Node.js的REPL里面執(zhí)行:

 

  1. console.time('timeout')  
  2. setTimeout(console.timeEnd, 100, 'timeout')  
  3. let n = 1e7  
  4. while (n--) Math.random() 

 

這個(gè)打印出的時(shí)間永遠(yuǎn)不會(huì)是100ms。它將是150ms到250ms之間的一個(gè)數(shù)字。setTimeoiut 調(diào)度了一個(gè)異步操作(console.timeEnd),但是當(dāng)前執(zhí)行的代碼沒有完成;下面有額外兩行代碼來做了一個(gè)循環(huán)。當(dāng)前所執(zhí)行的代碼通常被叫做“Tick”。要完成這個(gè) Tick,Math.random 需要被調(diào)用 1000 萬次。如果這會(huì)花銷 100ms,那么timeout觸發(fā)時(shí)的總時(shí)間就是 200ms (再加上setTimeout函數(shù)實(shí)際推入隊(duì)列時(shí)的延時(shí),約幾毫秒)

譯者注: 實(shí)際上這里作者的解釋有點(diǎn)小問題。首先這個(gè)例子假如按他所說循環(huán)會(huì)耗費(fèi)100毫秒,那么setTimeout觸發(fā)時(shí)也是100ms而已,不會(huì)是兩個(gè)時(shí)間相加。因?yàn)?00毫秒的循環(huán)結(jié)束,setTimeout也要被觸發(fā)了。

另外:你實(shí)際電腦測(cè)試時(shí),很可能像我一樣得到的結(jié)果是 100ms多一點(diǎn),而不是作者的150-250之間。作者之所以得到 150ms,是因?yàn)樗褂玫碾娔X性能原因使得 while(n--) 這個(gè)循環(huán)所花費(fèi)的時(shí)間是 150ms到250ms。而一旦性能好一點(diǎn)的電腦計(jì)算1e7次循環(huán)只需幾十毫秒,完全不會(huì)阻塞100毫秒之后的setTimeout,這時(shí)得到的結(jié)果往往是103ms左右,其中的3ms是底層函數(shù)入隊(duì)和調(diào)用花掉的時(shí)間(跟這里所說的問題無關(guān))。因此,你自己在測(cè)試時(shí)可以把1e7改成1e8試試。總之讓他的執(zhí)行時(shí)間超過100毫秒。

在服務(wù)器端上下文如果一個(gè)操作在當(dāng)前 Tick 中執(zhí)行時(shí)間很長(zhǎng),那么就會(huì)導(dǎo)致請(qǐng)求無法被處理,并且數(shù)據(jù)也無法獲取(譯者注:比如處理新的網(wǎng)絡(luò)請(qǐng)求或處理讀取文件的IO事件),因?yàn)楫惒酱a在當(dāng)前 Tick 完成之前無法執(zhí)行。這意味著計(jì)算昂貴的代碼將會(huì)讓server所有交互都變得緩慢。所以建議你拆分資源敏感的任務(wù)到單獨(dú)的進(jìn)程里去,然后從main主server中去調(diào)用它,這能避免那些很少使用但資源敏感(譯者注: 這里特指CPU敏感)的路由拖慢了那些經(jīng)常訪問但資源不敏感的路由的性能(譯者注:就是不要讓某個(gè)cpu密集的路徑拖慢整個(gè)node應(yīng)用)。

本文的例子server中有很多代碼阻塞了事件循環(huán),所以下一步我們來定位這個(gè)代碼的具體位置所在。

分析

定位性能問題的代碼的一個(gè)方法就是創(chuàng)建和分析“火焰圖”。一個(gè)火焰圖將函數(shù)表達(dá)為彼此疊加的塊---不是隨著時(shí)間的推移而是聚合。之所以叫火焰圖是因?yàn)樗瞄冱S到紅色的色階來表示,越紅的塊則表示是個(gè)“熱點(diǎn)”函數(shù),意味著很可能會(huì)阻塞事件循環(huán)。獲取火焰圖的數(shù)據(jù)需要通過對(duì)CPU進(jìn)行采樣---即node中當(dāng)前執(zhí)行的函數(shù)及其堆棧的快照。而熱量(heat)是由一個(gè)函數(shù)在分析期間處于棧頂執(zhí)行所占用的時(shí)間百分比決定的。如果它不是當(dāng)前棧中***被調(diào)用的那個(gè)函數(shù),那么他就很可能會(huì)阻塞事件循環(huán)。

讓我們用 clinic flame 來生成示例代碼的火焰圖:

 

  1. clinic flame --on-port=’autocannon -c100 localhost:$PORT/seed/v1’ -- node index.js 

譯者注: 也可以使用新版命令風(fēng)格:

 

  1. clinic flame --autocannon [ /seed/v1 -c200 -d 10 ] -- node index.js 

結(jié)果會(huì)自動(dòng)展示在你的瀏覽器中:

譯者注: 新版變成下面這副樣子了,功能更強(qiáng)大,但可能得學(xué)習(xí)下怎么看。。

(譯者注:下面分析時(shí)還是看原文的圖)

塊的寬度表示它花費(fèi)了多少CPU時(shí)間??梢钥吹?個(gè)主要堆?;ㄙM(fèi)了大部分的時(shí)間,而其中 server.on 這個(gè)是最紅的。 實(shí)際上,這3個(gè)堆棧是相同的。他們之所以分開是因?yàn)樵诜治銎陂g優(yōu)化過的和未優(yōu)化的函數(shù)會(huì)被視為不同的調(diào)用幀。帶有 * 前綴的是被JavaScript引擎優(yōu)化過的函數(shù),而帶有 ~ 前綴的是未優(yōu)化的。如果是否優(yōu)化對(duì)我們的分析不重要,我們可以點(diǎn)擊 Merge 按鈕把它們合并。這時(shí)圖像會(huì)變成這樣:

從開始看,我們可以發(fā)現(xiàn)出問題的代碼在 util.js 里。這個(gè)過慢的函數(shù)也是一個(gè) event handler:觸發(fā)這個(gè)函數(shù)的來源是Node核心里的 events 模塊,而 server.on 是event handler匿名函數(shù)的一個(gè)后備名稱。我們可以看到這個(gè)代碼跟實(shí)際處理本次request請(qǐng)求的代碼并不在同一個(gè) Tick 當(dāng)中(譯者注: 如果在同一個(gè)Tick就會(huì)用一個(gè)堆棧圖豎向堆疊起來)。如果跟request處理在同一個(gè) Tick中,那堆棧中應(yīng)該是Node的 http 模塊、net和stream模塊

如果你展開其他的更小的塊你會(huì)看到這些Http的Node核心函數(shù)。比如嘗試下右上角的search,搜索關(guān)鍵詞 send(restify和http內(nèi)部方法都有send方法)。然后你可以發(fā)現(xiàn)他們?cè)诨鹧鎴D的右邊(函數(shù)按字母排序)(譯者注:右側(cè)藍(lán)色高亮的區(qū)域)

可以看到實(shí)際的 HTTP 處理塊占用時(shí)間相對(duì)較少。

我們可以點(diǎn)擊一個(gè)高亮的青色塊來展開,看到里面 http_outgoing.js 文件的 writeHead、write函數(shù)(Node核心http庫中的一部分)

我們可以點(diǎn)擊 all stack 返回到主要視圖。

這里的關(guān)鍵點(diǎn)是,盡管 server.on 函數(shù)跟實(shí)際 request處理代碼不在一個(gè) Tick中,它依然能通過延遲其他正在執(zhí)行的代碼來影響了server的性能。

Debuging 調(diào)試

我們現(xiàn)在從火焰圖知道了問題函數(shù)在 util.js 的 server.on 這個(gè)eventHandler里。我們來瞅一眼:

 

  1. server.on('after', (req, res) => {  
  2.   if (res.statusCode !== 200) return  
  3.   if (!res._body) return  
  4.   const key = crypto.createHash('sha512')  
  5.     .update(req.url)  
  6.     .digest()  
  7.     .toString('hex')  
  8.   const etag = crypto.createHash('sha512')  
  9.     .update(JSON.stringify(res._body))  
  10.     .digest()  
  11.     .toString('hex')  
  12.   if (cache[key] !== etag) cache[key] = etag  
  13. }) 

 

眾所周知,加密過程都是很昂貴的cpu密集任務(wù),還有序列化(JSON.stringify),但是為什么火焰圖中看不到呢?實(shí)際上在采樣過程中都已經(jīng)被記錄了,只是他們隱藏在 cpp過濾器 內(nèi) (譯者注:cpp就是c++類型的代碼)。我們點(diǎn)擊 cpp 按鈕就能看到如下的樣子:

與序列化和加密相關(guān)的內(nèi)部V8指令被展示為最熱的區(qū)域堆棧,并且花費(fèi)了最多的時(shí)間。 JSON.stringify 方法直接調(diào)用了 C++代碼,這就是為什么我們看不到JavaScript 函數(shù)。在加密這里, createHash 和 update 這樣的函數(shù)都在數(shù)據(jù)中,而他們要么內(nèi)聯(lián)(合并并消失在merge視圖)要么占用時(shí)間太小無法展示。

一旦我們開始推理etagger函數(shù)中的代碼,很快就會(huì)發(fā)現(xiàn)它的設(shè)計(jì)很糟糕。為什么我們要從函數(shù)上下文中獲取服務(wù)器實(shí)例?所有這些hash計(jì)算都是必要的嗎?在實(shí)際場(chǎng)景中也沒有If-None-Match頭支持,如果用if-none-match這將減輕某些真實(shí)場(chǎng)景中的一些負(fù)載,因?yàn)榭蛻舳藭?huì)發(fā)出頭請(qǐng)求來確定資源的新鮮度。

讓我們先忽略所有這些問題,先驗(yàn)證一下 server.on 中的代碼是否是導(dǎo)致問題的原因。我們可以把 server.on 里面的代碼做成空函數(shù)然后生成一個(gè)新的火焰圖。

現(xiàn)在 etagger 函數(shù)變成這樣:

 

  1. function etagger () {  
  2.   var cache = {}  
  3.   var afterEventAttached = false  
  4.   function attachAfterEvent (server) {  
  5.     if (attachAfterEvent === true) return  
  6.     afterEventAttached = true  
  7.     server.on('after', (req, res) => {})  
  8.   }  
  9.   return function (req, res, next) {  
  10.     attachAfterEvent(this)  
  11.     const key = crypto.createHash('sha512')  
  12.       .update(req.url)  
  13.       .digest()  
  14.       .toString('hex')  
  15.     if (key in cache) res.set('Etag', cache[key])  
  16.     res.set('Cache-Control', 'public, max-age=120')  
  17.     next()  
  18.   }  

 

現(xiàn)在 server.on 的事件監(jiān)聽函數(shù)是個(gè)以空函數(shù) no-op. 讓我們?cè)俅螆?zhí)行 clinic flame:

 

  1. clinic flame --on-port='autocannon -c100 localhost:$PORT/seed/v1' -- node index.js 
  2.  
  3. Copy 

 

會(huì)生成如下的火焰圖:

這看起來好一些,我們會(huì)看到每秒吞吐量有所增長(zhǎng)。但是為什么 event emit 的代碼這么紅? 我們期望的是此時(shí) HTTP 處理要占用最多的CPU時(shí)間,畢竟 server.on 里面已經(jīng)什么都沒做了。

這種類型的瓶頸通常因?yàn)橐粋€(gè)函數(shù)調(diào)用超出了一定期望的程度。

util.js 頂部的這一句可疑的代碼可能是一個(gè)線索:

 

  1. require('events').defaultMaxListeners = Infinity 

讓我們移除掉這句代碼,然后啟動(dòng)我們的應(yīng)用,帶上 --trace-warnings flag標(biāo)記。

 

  1. node --trace-warnings index.js 

如果我們?cè)谙乱粋€(gè)teminal中執(zhí)行壓測(cè):

 

  1. autocannon -c100 localhost:3000/seed/v1 

會(huì)看到我們的進(jìn)程輸出一些:

 

  1. (node:96371) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 after listeners added. Use emitter.setMaxListeners() to increase limit  
  2.   at _addListener (events.js:280:19)  
  3.   at Server.addListener (events.js:297:10)  
  4.   at attachAfterEvent   
  5.     (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:22:14)  
  6.   at Server.  
  7.     (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:25:7)  
  8.   at call  
  9.     (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:164:9)  
  10.   at next  
  11.     (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:120:9)  
  12.   at Chain.run  
  13.     (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:123:5)  
  14.   at Server._runUse  
  15.     (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:976:19)  
  16.   at Server._runRoute  
  17.     (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:918:10)  
  18.   at Server._afterPre  
  19.     (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:888:10) 

 

Node 告訴我們有太多的事件添加到了 server 對(duì)象上。這很奇怪,因?yàn)槲覀冇幸痪渑袛?,如?after 事件已經(jīng)綁定到了 server,則直接return。所以***綁定之后,只有一個(gè) no-op 函數(shù)綁到了 server上。

讓我們看下 attachAfterEvent 函數(shù):

 

  1. var afterEventAttached = false  
  2. function attachAfterEvent (server) {  
  3.   if (attachAfterEvent === true) return  
  4.   afterEventAttached = true  
  5.   server.on('after', (req, res) => {})  

 

我們發(fā)現(xiàn)條件檢查語句寫錯(cuò)了! 不應(yīng)該是 attachAfterEvent ,而是 afterEventAttached. 這意味著每個(gè)請(qǐng)求都會(huì)往 server 對(duì)象上添加一個(gè)事件監(jiān)聽,然后每個(gè)請(qǐng)求的***所有的之前綁定上的事件都要觸發(fā)。唉呀媽呀!

優(yōu)化

既然知道了問題所在,讓我們看看如何讓我們的server更快

低端優(yōu)化 (容易摘到的果子)

讓我們還原 server.on 的代碼(不讓他是空函數(shù)了)然后條件語句中改成正確的 boolean 判斷。現(xiàn)在我們的 etagger 函數(shù)這樣:

 

  1. function etagger () {  
  2.   var cache = {}  
  3.   var afterEventAttached = false  
  4.   function attachAfterEvent (server) {  
  5.     if (afterEventAttached === true) return  
  6.     afterEventAttached = true  
  7.     server.on('after', (req, res) => {  
  8.       if (res.statusCode !== 200) return  
  9.       if (!res._body) return  
  10.       const key = crypto.createHash('sha512')  
  11.         .update(req.url)  
  12.         .digest()  
  13.         .toString('hex')  
  14.       const etag = crypto.createHash('sha512')  
  15.         .update(JSON.stringify(res._body))  
  16.         .digest()  
  17.         .toString('hex')  
  18.       if (cache[key] !== etag) cache[key] = etag  
  19.     })  
  20.   }  
  21.   return function (req, res, next) {  
  22.     attachAfterEvent(this)  
  23.     const key = crypto.createHash('sha512')  
  24.       .update(req.url)  
  25.       .digest()  
  26.       .toString('hex')  
  27.     if (key in cache) res.set('Etag', cache[key])  
  28.     res.set('Cache-Control', 'public, max-age=120')  
  29.     next()  
  30.   }  

 

現(xiàn)在,我們?cè)賮韴?zhí)行一次 Profile(進(jìn)程剖析,進(jìn)程描述)。

 

  1. node index.js 

然后用 autocanno 來profile 它:

 

  1. autocannon -c100 localhost:3000/seed/v1 

我們看到結(jié)果顯示有200倍的提升(持續(xù)10秒 100個(gè)并發(fā))

平衡開發(fā)成本和潛在的服務(wù)器成本也非常重要。我們需要定義我們?cè)趦?yōu)化時(shí)要走多遠(yuǎn)。否則我們很容易將80%的時(shí)間投入到20%的性能提高上。項(xiàng)目是否能承受?

在一些場(chǎng)景下,用 低端優(yōu)化 來花費(fèi)一天提高200倍速度才被認(rèn)為是合理的。而在某些情況下,我們可能希望不惜一切讓我們的項(xiàng)目盡*********可能的快。這種抉擇要取決于項(xiàng)目?jī)?yōu)先級(jí)。

控制資源支出的一種方法是設(shè)定目標(biāo)。例如,提高10倍,或達(dá)到每秒4000次請(qǐng)求。基于業(yè)務(wù)需求的這一種方式最有意義。例如,如果服務(wù)器成本超出預(yù)算100%,我們可以設(shè)定2倍改進(jìn)的目標(biāo)

更進(jìn)一步

如果我們?cè)僮鲆粡埢鹧鎴D,我們會(huì)看到:

事件監(jiān)聽器依然是一個(gè)瓶頸,它依然占用了 1/3 的CPU時(shí)間 (它的寬度大約是整行的三分之一)

(譯者注: 在做優(yōu)化之前可能每次都要做這樣的思考:) 通過優(yōu)化我們能獲得哪些額外收益,以及這些改變(包括相關(guān)聯(lián)的代碼重構(gòu))是否值得?

==============

我們看最終***優(yōu)化(譯者注:***優(yōu)化指的是作者在后文提到的另外一些方法)后能達(dá)到的性能特征(持續(xù)執(zhí)行十秒 http://localhost:3000/seed/v1 --- 100個(gè)并發(fā)連接)

92k requests in 11s, 937.22 MB read[15]

盡管***優(yōu)化后 1.6倍 的性能提高已經(jīng)很顯著了,但與之付出的努力、改變、代碼重構(gòu) 是否有必要也是值得商榷的。尤其是與之前簡(jiǎn)單修復(fù)一個(gè)bug就能提升200倍的性能相比。

為了實(shí)現(xiàn)深度改進(jìn),需要使用同樣的技術(shù)如:profile分析、生成火焰圖、分析、debug、優(yōu)化。***完成優(yōu)化后的服務(wù)器代碼,可以在這里查看。

***提高到 800/s 的吞吐量,使用了如下方法:

  • 不要先創(chuàng)建對(duì)象然后再序列化,不如創(chuàng)建時(shí)就直接創(chuàng)建為字符串。
  • 用一些其他的唯一的東西來標(biāo)識(shí)etag,而不是創(chuàng)建hash。
  • 不要對(duì)url進(jìn)行hash,直接用url當(dāng)key。

這些更改稍微復(fù)雜一些,對(duì)代碼庫的破壞性稍大一些,并使etagger中間件的靈活性稍微降低,因?yàn)樗鼤?huì)給路由帶來負(fù)擔(dān)以提供Etag值。但它在執(zhí)行Profile的機(jī)器上每秒可多增加3000個(gè)請(qǐng)求。

讓我們看看最終優(yōu)化后的火焰圖:

圖中最熱點(diǎn)的地方是 Node core(node核心)的 net 模塊。這是最期望的情況。

防止性能問題

***一點(diǎn),這里提供一些在部署之前防止性能問題的建議。

在開發(fā)期間使用性能工具作為非正式檢查點(diǎn)可以避免把性能問題帶入生產(chǎn)環(huán)境。建議將AutoCannon和Clinic(或其他類似的工具)作為日常開發(fā)工具的一部分。

購(gòu)買或使用一個(gè)框架時(shí),看看他的性能政策是什么(譯者注:對(duì)開源框架就看看benchmark和文檔中的性能建議)。如果框架沒有指出性能相關(guān)的,那么就看看他是否與你的基礎(chǔ)架構(gòu)和業(yè)務(wù)目標(biāo)一致。例如,Restify已明確(自版本7發(fā)布以來)將致力于提升性。但是,如果低成本和高速度是你絕對(duì)優(yōu)先考慮的問題,請(qǐng)考慮使用Fastify,Restify貢獻(xiàn)者測(cè)得的速度提高17%。

在選擇一些廣泛流行的類庫時(shí)要多加留意---尤其是留意日志。 在開發(fā)者修復(fù)issue的時(shí)候,他們可能會(huì)在代碼中添加一些日志輸出來幫助他們?cè)谖磥韉ebug問題。如果她用了一個(gè)性能差勁的 logger 組件,這可能會(huì)像 溫水煮青蛙 一樣隨著時(shí)間的推移扼殺性能。pino 日志組件是一個(gè) Node.js 中可以用的速度最快的JSON換行日志組件。

***,始終記住Event Loop是一個(gè)共享資源。 Node.js服務(wù)器的性能會(huì)受到最熱路徑中最慢的那個(gè)邏輯的約束。

 

責(zé)任編輯:龐桂玉 來源: segmentfault
相關(guān)推薦

2013-11-01 09:34:56

Node.js技術(shù)

2015-03-10 10:59:18

Node.js開發(fā)指南基礎(chǔ)介紹

2014-02-19 16:28:53

Node.jsWeb工具

2015-11-04 09:18:41

Node.js應(yīng)用性能

2020-05-29 15:33:28

Node.js框架JavaScript

2021-12-25 22:29:57

Node.js 微任務(wù)處理事件循環(huán)

2012-02-03 09:25:39

Node.js

2011-09-09 14:23:13

Node.js

2011-11-01 10:30:36

Node.js

2011-09-08 13:46:14

node.js

2011-09-02 14:47:48

Node

2012-10-24 14:56:30

IBMdw

2011-11-10 08:55:00

Node.js

2021-08-24 06:38:37

Node.js COW 文件復(fù)制

2011-11-02 09:04:15

Node.js

2021-09-26 05:06:04

Node.js模塊機(jī)制

2021-11-06 18:40:27

js底層模塊

2024-09-25 08:04:58

2020-12-14 15:40:59

Nodefastifyjs

2020-12-28 08:48:44

JS工具fastify
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)