pre-tips
本文翻譯自: Keeping Node.js Fast: Tools, Techniques, And Tips For Making High-Performance Node.js Servers
原文地址:https://www.smashingmagazine....
中文標題:保持Node.js的速度-建立高效能Node.js Servers的工具、技術和提示
快速摘要
Node 是一個非常多彩的平臺,而建立network服務就是其非常重要的能力之一。在本文我們將關注最主流的: HTTP Web servers.
引子
如果你已經使用Node.js足夠長的時間,那麼毫無疑問你會碰到比較痛苦的速度問題。JavaScript是一種事件驅動的、非同步的語言。這很明顯使得對效能的推理變得棘手。Node.js的迅速普及使得我們必須尋找適合這種server-side javacscript的工具、技術。
當我們碰到效能問題,在瀏覽器端的經驗將無法適用於伺服器端。所以我們如何確保一個Node.js程式碼是快速的且能達到我們的要求呢?讓我們來動手看一些例項
工具
我們需要一個工具來壓測我們的server從而測量效能。比如,我們使用 autocannon
npm install -g autocannon // 或使用淘寶源cnpm, 騰訊源tnpm
其他的Http benchmarking tools 包括 Apache Bench(ab) 和 wrk2, 但AutoCannon是用Node寫的,對前端來說會更加方便並易於安裝,它可以非常方便的安裝在 Windows、Linux 和Mac OS X.
當我們安裝了基準效能測試工具,我們需要用一些方法去診斷我們的程式。一個很不錯的診斷效能問題的工具便是 Node Clinic 。它也可以用npm安裝:
npm install -g clinic
這實際上會安裝一系列套件,我們將使用 Clinic Doctor
和 Clinic Flame (一個 ox 的封裝)
譯者注: ox是一個自動剖析cpu並生成node程式火焰圖的工具; 而clinic Flame就是基於ox的封裝。
另一方面, clinic工具本身其實是一系列套件的組合,它不同的子命令分別會呼叫到不同的子模組,例如:
- 醫生診斷功能。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: 對於本文例項,需要 Node 8.11.2 或更高版本
程式碼示例
我們的例子是一個只有一個資源的簡單的 REST server:暴露一個 GET 訪問的路由 /seed/v1
,返回一個大 JSON 載荷。 server端的程式碼就是一個app目錄,裡面包括一個 packkage.json
(依賴 restify 7.1.0
)、一個 index.js
和 一個 util.js
(譯者注: 放一些工具函式)
// index.js
const restify = require('restify')
const server = restify.createServer()
const { etagger, timestamp, fetchContent } from './util'
server.use(etagger.bind(server)) // 繫結etagger中介軟體,可以給資源請求加上etag響應頭
server.get('/seed/v1', function () {
fetchContent(req.url, (err, content) => {
if (err) {
return next(err)
}
res.send({data: content, ts: timestamp(), url: req.url})
next()
})
})
server.listen(8080, function () {
cosnole.log(' %s listening at %s', server.name, server.url)
})
// util.js
const restify = require('restify')
const crypto = require('crypto')
module.exports = function () {
const content = crypto.rng('5000').toString('hex') // 普通有規則的隨機
const fetchContent = function (url, cb) {
setImmediate(function () {
if (url !== '/seed/v1') return restify.errors.NotFoundError('no api!')
cb(content)
})
}
let last = Date.now()
const TIME_ONE_MINUTE = 60000
const timestamp = function () {
const now = Date.now()
if (now - last >= TIME_ONE_MINITE) {
last = now
}
return last
}
const etagger = function () {
const cache = {}
let afterEventAttached = false
function attachAfterEvent(server) {
if (attachAfterEvent ) return
afterEventAttached = true
server.on('after', function (req, res) {
if (res.statusCode == 200 && res._body != null) {
const urlKey = crpto.createHash('sha512')
.update(req.url)
.digets()
.toString('hex')
const contentHash = crypto.createHash('sha512')
.update(JSON.stringify(res._body))
.digest()
.toString('hex')
if (cache[urlKey] != contentHash) cache[urlKey] = contentHash
}
})
}
return function(req, res, next) {
// 譯者注: 這裡attachEvent的位置好像不太優雅,我換另一種方式改了下這裡。可以參考: https://github.com/cuiyongjian/study-restify/tree/master/app
attachAfterEvent(this) // 給server註冊一個after鉤子,每次即將響應資料時去計算body的etag值
const urlKey = crypto.createHash('sha512')
.update(req.url)
.digest()
.toString('hex')
// 譯者注: 這裡etag的返回邏輯應該有點小問題,每次請求都是返回的上次寫入cache的etag
if (urlKey in cache) res.set('Etag', cache[urlKey])
res.set('Cache-Control', 'public; max-age=120')
}
}
return { fetchContent, timestamp, etagger }
}
務必不要用這段程式碼作為最佳實踐,因為這裡面有很多程式碼的壞味道,但是我們接下來將測量並找出這些問題。
要獲得這個例子的原始碼可以去這裡
Profiling 剖析
為了剖析我們的程式碼,我們需要兩個終端視窗。一個用來啟動app,另外一個用來壓測他。
第一個terminal,我們執行:
node ./index.js
另外一個terminal,我們這樣剖析他(譯者注: 實際是在壓測):
autocannon -c100 localhost:3000/seed/v1
這將開啟100個併發請求轟炸服務,持續10秒。
結果大概是下面這個樣子:
stat | avg | stdev | Max |
---|---|---|---|
耗時(毫秒) | 3086.81 | 1725.2 | 5554 |
吞吐量(請求/秒) | 23.1 | 19.18 | 65 |
每秒傳輸量(位元組/秒) | 237.98 kB | 197.7 kB | 688.13 kB |
231 requests in 10s, 2.4 MB read
結果會根據你機器情況變化。然而我們知道: 一般的“Hello World”Node.js伺服器很容易在同樣的機器上每秒完成三萬個請求,現在這段程式碼只能承受每秒23個請求且平均延遲超過3秒,這是令人沮喪的。
譯者注: 我用公司macpro18款 15寸 16G 256G,測試結果如下:
診斷
定位問題
我們可以通過一句命令來診斷應用,感謝 clinic doctor 的 -on-port 命令。在app目錄下,我們執行:
clinic doctor --on-port='autocannon -c100 localhost:3000/seed/v1' -- node index.js
譯者注:
現在autocannon的話可以使用新的subarg形式的命令語法:
clinic doctor --autocannon [ /seed/v1 -c 100 ] -- node index.js
clinic doctor會在剖析完畢後,建立html檔案並自動開啟瀏覽器。
結果長這個樣子:
譯者的測試長這樣子:
譯者注:橫座標其實是你係統時間,冒號後面的表示當前的系統時間的 - 秒數。
備註:接下來的文章內容分析,我們還是以原文的統計結果圖片為依據。
跟隨UI頂部的訊息,我們看到 EventLoop 圖表,它的確是紅色的,並且這個EventLoop延遲在持續增長。在我們深入研究他意味著什麼之前,我們先了解下其他指標下的診斷。
我們可以看到CPU一直在100%或超過100%這裡徘徊,因為程式正在努力處理排隊的請求。Node的 JavaScript 引擎(也就是V8) 著這裡實際上用 2 個 CPU核心在工作,因為機器是多核的 而V8會用2個執行緒。 一個執行緒用來執行 EventLoop,另外一個執行緒用來垃圾收集。 當CPU高達120%的時候就是程式在回收處理完的請求的遺留物件了(譯者注: 作業系統的程式CPU使用率的確經常會超過100%,這是因為程式內用了多執行緒,OS把工作分配到了多個核心,因此統計cpu佔用時間時會超過100%)
我們看與之相關的記憶體圖表。實線表示記憶體的堆記憶體佔用(譯者注:RSS表示node程式實際佔用的記憶體,heapUsage堆記憶體佔用就是指的堆區域佔用了多少,THA就表示總共申請到了多少堆記憶體。一般看heapUsage就好,因為他表示了node程式碼中大多數JavaScript物件所佔用的記憶體)。我們看到,只要CPU圖表上升一下則堆記憶體佔用就下降一些,這表示記憶體正在被回收。
activeHandler跟EventLoop的延遲沒有什麼相關性。一個active hanlder 就是一個表達 I/O的物件(比如socket或檔案控制程式碼) 或者一個timer (比如setInterval)。我們用autocannon建立了100連線的請求(-c100), activehandlers 保持在103. 額外的3個handler其實是 STDOUT,STDERROR 以及 server 物件自身(譯者: server自身也是個socket監聽控制程式碼)。
如果我們點選一下UI介面上底部的建議pannel皮膚,我們會看到:
短期緩解
深入分析效能問題需要花費大量的時間。在一個現網專案中,可以給伺服器或服務新增過載保護。過載保護的思路就是檢測 EventLoop 延遲(以及其他指標),然後在超過閾值時響應一個 "503 Service Unavailable"。這就可以讓 負載均衡器轉向其他server例項,或者實在不行就讓使用者過一會重試。overload-protection-module 這個過載保護模組能直接低成本地接入到 Express、Koa 和 Restify使用。Hapi 框架也有一個配置項提供同樣的過載保護。(譯者注:實際上看overload-protection模組的底層就是通過loopbench 實現的EventLoop延遲取樣,而loopbench就是從Hapi框架裡抽離出來的一個模組;至於記憶體佔用,則是overload-protection內部自己實現的取樣,畢竟直接用memoryUsage的api就好了)
理解問題所在
就像 Clinic Doctor 說的,如果 EventLoop 延遲到我們觀察的這個樣子,很可能有一個或多個函式阻塞了事件迴圈。認識到Node.js的這個主要特性非常重要:在當前的同步程式碼執行完成之前,非同步事件是無法被執行的。這就是為什麼下面 setTimeout 不能按照預料的時間觸發的原因。
舉例,在瀏覽器開發者工具或Node.js的REPL裡面執行:
console.time('timeout')
setTimeout(console.timeEnd, 100, 'timeout')
let n = 1e7
while (n--) Math.random()
這個列印出的時間永遠不會是100ms。它將是150ms到250ms之間的一個數字。setTimeoiut
排程了一個非同步操作(console.timeEnd),但是當前執行的程式碼沒有完成;下面有額外兩行程式碼來做了一個迴圈。當前所執行的程式碼通常被叫做“Tick”。要完成這個 Tick,Math.random 需要被呼叫 1000 萬次。如果這會花銷 100ms,那麼timeout觸發時的總時間就是 200ms (再加上setTimeout函式實際推入佇列時的延時,約幾毫秒)
譯者注: 實際上這裡作者的解釋有點小問題。首先這個例子假如按他所說迴圈會耗費100毫秒,那麼setTimeout觸發時也是100ms而已,不會是兩個時間相加。因為100毫秒的迴圈結束,setTimeout也要被觸發了。
另外:你實際電腦測試時,很可能像我一樣得到的結果是 100ms多一點,而不是作者的150-250之間。作者之所以得到 150ms,是因為它使用的電腦效能原因使得 while(n--) 這個迴圈所花費的時間是 150ms到250ms。而一旦效能好一點的電腦計算1e7次迴圈只需幾十毫秒,完全不會阻塞100毫秒之後的setTimeout,這時得到的結果往往是103ms左右,其中的3ms是底層函式入隊和呼叫花掉的時間(跟這裡所說的問題無關)。因此,你自己在測試時可以把1e7改成1e8試試。總之讓他的執行時間超過100毫秒。
在伺服器端上下文如果一個操作在當前 Tick 中執行時間很長,那麼就會導致請求無法被處理,並且資料也無法獲取(譯者注:比如處理新的網路請求或處理讀取檔案的IO事件),因為非同步程式碼在當前 Tick 完成之前無法執行。這意味著計算昂貴的程式碼將會讓server所有互動都變得緩慢。所以建議你拆分資源敏感的任務到單獨的程式裡去,然後從main主server中去呼叫它,這能避免那些很少使用但資源敏感(譯者注: 這裡特指CPU敏感)的路由拖慢了那些經常訪問但資源不敏感的路由的效能(譯者注:就是不要讓某個cpu密集的路徑拖慢整個node應用)。
本文的例子server中有很多程式碼阻塞了事件迴圈,所以下一步我們來定位這個程式碼的具體位置所在。
分析
定位效能問題的程式碼的一個方法就是建立和分析“火焰圖”。一個火焰圖將函式表達為彼此疊加的塊---不是隨著時間的推移而是聚合。之所以叫火焰圖是因為它用橘黃到紅色的色階來表示,越紅的塊則表示是個“熱點”函式,意味著很可能會阻塞事件迴圈。獲取火焰圖的資料需要通過對CPU進行取樣---即node中當前執行的函式及其堆疊的快照。而熱量(heat)是由一個函式在分析期間處於棧頂執行所佔用的時間百分比決定的。如果它不是當前棧中最後被呼叫的那個函式,那麼他就很可能會阻塞事件迴圈。
讓我們用 clinic flame
來生成示例程式碼的火焰圖:
clinic flame --on-port=’autocannon -c100 localhost:$PORT/seed/v1’ -- node index.js
譯者注: 也可以使用新版命令風格:
clinic flame --autocannon [ /seed/v1 -c200 -d 10 ] -- node index.js
結果會自動展示在你的瀏覽器中:
譯者注: 新版變成下面這副樣子了,功能更強大,但可能得學習下怎麼看。。
(譯者注:下面分析時還是看原文的圖)
塊的寬度表示它花費了多少CPU時間。可以看到3個主要堆疊花費了大部分的時間,而其中 server.on
這個是最紅的。 實際上,這3個堆疊是相同的。他們之所以分開是因為在分析期間優化過的和未優化的函式會被視為不同的呼叫幀。帶有 *
字首的是被JavaScript引擎優化過的函式,而帶有 ~
字首的是未優化的。如果是否優化對我們的分析不重要,我們可以點選 Merge
按鈕把它們合併。這時影象會變成這樣:
從開始看,我們可以發現出問題的程式碼在 util.js
裡。這個過慢的函式也是一個 event handler:觸發這個函式的來源是Node核心裡的 events
模組,而 server.on
是event handler匿名函式的一個後備名稱。我們可以看到這個程式碼跟實際處理本次request請求的程式碼並不在同一個 Tick 當中(譯者注: 如果在同一個Tick就會用一個堆疊圖豎向堆疊起來)。如果跟request處理在同一個 Tick中,那堆疊中應該是Node的 http
模組、net和stream模組
如果你展開其他的更小的塊你會看到這些Http的Node核心函式。比如嘗試下右上角的search,搜尋關鍵詞 send
(restify和http內部方法都有send方法)。然後你可以發現他們在火焰圖的右邊(函式按字母排序)(譯者注:右側藍色高亮的區域)
可以看到實際的 HTTP 處理塊佔用時間相對較少。
我們可以點選一個高亮的青色塊來展開,看到裡面 http_outgoing.js
檔案的 writeHead、write函式(Node核心http庫中的一部分)
我們可以點選 all stack
返回到主要檢視。
這裡的關鍵點是,儘管 server.on
函式跟實際 request處理程式碼不在一個 Tick中,它依然能通過延遲其他正在執行的程式碼來影響了server的效能。
Debuging 除錯
我們現在從火焰圖知道了問題函式在 util.js 的 server.on
這個eventHandler裡。我們來瞅一眼:
server.on('after', (req, res) => {
if (res.statusCode !== 200) return
if (!res._body) return
const key = crypto.createHash('sha512')
.update(req.url)
.digest()
.toString('hex')
const etag = crypto.createHash('sha512')
.update(JSON.stringify(res._body))
.digest()
.toString('hex')
if (cache[key] !== etag) cache[key] = etag
})
眾所周知,加密過程都是很昂貴的cpu密集任務,還有序列化(JSON.stringify),但是為什麼火焰圖中看不到呢?實際上在取樣過程中都已經被記錄了,只是他們隱藏在 cpp過濾器
內 (譯者注:cpp就是c++型別的程式碼)。我們點選 cpp 按鈕
就能看到如下的樣子:
與序列化和加密相關的內部V8指令被展示為最熱的區域堆疊,並且花費了最多的時間。 JSON.stringify
方法直接呼叫了 C++程式碼,這就是為什麼我們看不到JavaScript 函式。在加密這裡, createHash
和 update
這樣的函式都在資料中,而他們要麼內聯(合併並消失在merge檢視)要麼佔用時間太小無法展示。
一旦我們開始推理etagger函式中的程式碼,很快就會發現它的設計很糟糕。為什麼我們要從函式上下文中獲取伺服器例項?所有這些hash計算都是必要的嗎?在實際場景中也沒有If-None-Match頭支援,如果用if-none-match這將減輕某些真實場景中的一些負載,因為客戶端會發出頭請求來確定資源的新鮮度。
讓我們先忽略所有這些問題,先驗證一下 server.on
中的程式碼是否是導致問題的原因。我們可以把 server.on
裡面的程式碼做成空函式然後生成一個新的火焰圖。
現在 etagger 函式變成這樣:
function etagger () {
var cache = {}
var afterEventAttached = false
function attachAfterEvent (server) {
if (attachAfterEvent === true) return
afterEventAttached = true
server.on('after', (req, res) => {})
}
return function (req, res, next) {
attachAfterEvent(this)
const key = crypto.createHash('sha512')
.update(req.url)
.digest()
.toString('hex')
if (key in cache) res.set('Etag', cache[key])
res.set('Cache-Control', 'public, max-age=120')
next()
}
}
現在 server.on
的事件監聽函式是個以空函式 no-op. 讓我們再次執行 clinic flame:
clinic flame --on-port='autocannon -c100 localhost:$PORT/seed/v1' -- node index.js
Copy
會生成如下的火焰圖:
這看起來好一些,我們會看到每秒吞吐量有所增長。但是為什麼 event emit 的程式碼這麼紅? 我們期望的是此時 HTTP 處理要佔用最多的CPU時間,畢竟 server.on 裡面已經什麼都沒做了。
這種型別的瓶頸通常因為一個函式呼叫超出了一定期望的程度。
util.js
頂部的這一句可疑的程式碼可能是一個線索:
require('events').defaultMaxListeners = Infinity
讓我們移除掉這句程式碼,然後啟動我們的應用,帶上 --trace-warnings
flag標記。
node --trace-warnings index.js
如果我們在下一個teminal中執行壓測:
autocannon -c100 localhost:3000/seed/v1
會看到我們的程式輸出一些:
(node:96371) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 after listeners added. Use emitter.setMaxListeners() to increase limit
at _addListener (events.js:280:19)
at Server.addListener (events.js:297:10)
at attachAfterEvent
(/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:22:14)
at Server.
(/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:25:7)
at call
(/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:164:9)
at next
(/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:120:9)
at Chain.run
(/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:123:5)
at Server._runUse
(/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:976:19)
at Server._runRoute
(/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:918:10)
at Server._afterPre
(/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:888:10)
Node 告訴我們有太多的事件新增到了 server 物件上。這很奇怪,因為我們有一句判斷,如果 after
事件已經繫結到了 server,則直接return。所以首次繫結之後,只有一個 no-op 函式綁到了 server上。
讓我們看下 attachAfterEvent
函式:
var afterEventAttached = false
function attachAfterEvent (server) {
if (attachAfterEvent === true) return
afterEventAttached = true
server.on('after', (req, res) => {})
}
我們發現條件檢查語句寫錯了! 不應該是 attachAfterEvent
,而是 afterEventAttached
. 這意味著每個請求都會往 server 物件上新增一個事件監聽,然後每個請求的最後所有的之前繫結上的事件都要觸發。唉呀媽呀!
優化
既然知道了問題所在,讓我們看看如何讓我們的server更快
低端優化 (容易摘到的果子)
讓我們還原 server.on
的程式碼(不讓他是空函式了)然後條件語句中改成正確的 boolean 判斷。現在我們的 etagger 函式這樣:
function etagger () {
var cache = {}
var afterEventAttached = false
function attachAfterEvent (server) {
if (afterEventAttached === true) return
afterEventAttached = true
server.on('after', (req, res) => {
if (res.statusCode !== 200) return
if (!res._body) return
const key = crypto.createHash('sha512')
.update(req.url)
.digest()
.toString('hex')
const etag = crypto.createHash('sha512')
.update(JSON.stringify(res._body))
.digest()
.toString('hex')
if (cache[key] !== etag) cache[key] = etag
})
}
return function (req, res, next) {
attachAfterEvent(this)
const key = crypto.createHash('sha512')
.update(req.url)
.digest()
.toString('hex')
if (key in cache) res.set('Etag', cache[key])
res.set('Cache-Control', 'public, max-age=120')
next()
}
}
現在,我們再來執行一次 Profile(程式剖析,程式描述)。
node index.js
然後用 autocanno 來profile 它:
autocannon -c100 localhost:3000/seed/v1
我們看到結果顯示有200倍的提升(持續10秒 100個併發)
平衡開發成本和潛在的伺服器成本也非常重要。我們需要定義我們在優化時要走多遠。否則我們很容易將80%的時間投入到20%的效能提高上。專案是否能承受?
在一些場景下,用 低端優化
來花費一天提高200倍速度才被認為是合理的。而在某些情況下,我們可能希望不惜一切讓我們的專案盡最大最大最大可能的快。這種抉擇要取決於專案優先順序。
控制資源支出的一種方法是設定目標。例如,提高10倍,或達到每秒4000次請求。基於業務需求的這一種方式最有意義。例如,如果伺服器成本超出預算100%,我們可以設定2倍改進的目標
更進一步
如果我們再做一張火焰圖,我們會看到:
事件監聽器依然是一個瓶頸,它依然佔用了 1/3 的CPU時間 (它的寬度大約是整行的三分之一)
(譯者注: 在做優化之前可能每次都要做這樣的思考:) 通過優化我們能獲得哪些額外收益,以及這些改變(包括相關聯的程式碼重構)是否值得?
==============
我們看最終終極優化(譯者注:終極優化指的是作者在後文提到的另外一些方法)後能達到的效能特徵(持續執行十秒 http://localhost:3000/seed/v1 --- 100個併發連線)
92k requests in 11s, 937.22 MB read[15]
儘管終極優化後 1.6倍 的效能提高已經很顯著了,但與之付出的努力、改變、程式碼重構 是否有必要也是值得商榷的。尤其是與之前簡單修復一個bug就能提升200倍的效能相比。
為了實現深度改進,需要使用同樣的技術如:profile分析、生成火焰圖、分析、debug、優化。最後完成優化後的伺服器程式碼,可以在這裡檢視。
最後提高到 800/s 的吞吐量,使用瞭如下方法:
- 不要先建立物件然後再序列化,不如建立時就直接建立為字串。
- 用一些其他的唯一的東西來標識etag,而不是建立hash。
- 不要對url進行hash,直接用url當key。
這些更改稍微複雜一些,對程式碼庫的破壞性稍大一些,並使etagger中介軟體的靈活性稍微降低,因為它會給路由帶來負擔以提供Etag值。但它在執行Profile的機器上每秒可多增加3000個請求。
讓我們看看最終優化後的火焰圖:
圖中最熱點的地方是 Node core(node核心)的 net 模組。這是最期望的情況。
防止效能問題
完美一點,這裡提供一些在部署之前防止效能問題的建議。
在開發期間使用效能工具作為非正式檢查點可以避免把效能問題帶入生產環境。建議將AutoCannon和Clinic(或其他類似的工具)作為日常開發工具的一部分。
購買或使用一個框架時,看看他的效能政策是什麼(譯者注:對開源框架就看看benchmark和文件中的效能建議)。如果框架沒有指出效能相關的,那麼就看看他是否與你的基礎架構和業務目標一致。例如,Restify已明確(自版本7釋出以來)將致力於提升性。但是,如果低成本和高速度是你絕對優先考慮的問題,請考慮使用Fastify,Restify貢獻者測得的速度提高17%。
在選擇一些廣泛流行的類庫時要多加留意---尤其是留意日誌。 在開發者修復issue的時候,他們可能會在程式碼中新增一些日誌輸出來幫助他們在未來debug問題。如果她用了一個效能差勁的 logger 元件,這可能會像 溫水煮青蛙
一樣隨著時間的推移扼殺效能。pino 日誌元件是一個 Node.js 中可以用的速度最快的JSON換行日誌元件。
最後,始終記住Event Loop是一個共享資源。 Node.js伺服器的效能會受到最熱路徑中最慢的那個邏輯的約束。