Node.js 診斷指南 第一彈

大廠技術(shù) 高級前端 Node進階
點擊上方 程序員成長指北,關(guān)注公眾號
回復(fù)1,加入高級Node交流群
TL;DR
本文介紹的診斷小技巧有:調(diào)試環(huán)境變量、進程退出碼、廢棄 API 警告、識別同步 I/O 和處理 Unhandled Promise Rejection。
調(diào)試環(huán)境變量
以筆者最初使用的 Node.js 版本 v0.10.x 來說,當(dāng)時內(nèi)置的 util 還沒有提供 util.debuglog 方法,只有一個的 util.debug 效果等同于 console.error,而在當(dāng)時已經(jīng)開始要流行的是使用 TJ 的 debug 包,例如:
var debug = require('debug')('http')
, http = require('http')
, name = 'My App';
// fake app
debug('booting %o', name);
http.createServer(function(req, res){
debug(req.method + ' ' + req.url);
res.end('hello\n');
}).listen(3000, function(){
debug('listening');
});
通過 DEBUG 環(huán)境變量指定了 http 這個 label,就可以把代碼中 label 為 http 的調(diào)試日志輸出:

在 TJ 的設(shè)想里,只要開發(fā)者所寫的所有模塊及其依賴的模塊都使用這種方式,那么調(diào)試的時候大家都可以使用 DEBUG 這個 env 來過濾和開啟所有人的 debug 日志。
由于 TJ 的這個模塊出來實在是太早了(10年前),所以基本上已經(jīng)成為了社區(qū)的一個默認(rèn)約定,大部分流行的 npm 包內(nèi)部都會使用這個模塊來輸出調(diào)試日志(而不是 util.debuglog)。
以國內(nèi)的 eggjs 框架為例,我們直接啟動的日志如下:

通過 DEBUG 環(huán)境變量可以開啟指定模塊的內(nèi)部調(diào)試日志:

PS: 通過 DEBUG=* 就可以開啟所有的日志。
實際上 Node.js 也內(nèi)置了類似的機制,不過目前普遍認(rèn)為 Node.js 內(nèi)置的環(huán)境變量主要是用來排查 Node.js 內(nèi)置的代碼和模塊用的。這里主要有兩個環(huán)境變量分別對應(yīng)了 Node.js 內(nèi)置的 JavaScript 層以及 C++ 層的調(diào)試日志,分別是:
NODE_DEBUG 用于開啟 JavaScript 調(diào)試日志開關(guān)(也包括用戶使用 util.debuglog 的部分)
NODE_DEBUG_NATIVE 用戶開啟 C++ 調(diào)試日志開關(guān)
常見的 NODE_DEBUG 開關(guān):
timer
http
net
fs
cluster
tls
stream
child_process
module
以上文中的 http 代碼為例,同時開啟 DEBUG 和 NODE_DEBUG 效果:

上圖中,我們開啟了 Node.js 內(nèi)置的 net 模塊的日志初始,之后每次 http 請求進來,net 模塊的 debug 日志都會詳細(xì)輸出(由于日志內(nèi)容比較多所以沒有放出來請求的例子圖,大家可以自行測試)。
進程退出碼
有的時候我們的 Node.js 進程直接退出了,如果沒有收集到足夠錯誤日志,可以根據(jù)進程的退出碼輔助判斷錯誤情況。下面引用 Node.js 文檔中的內(nèi)容:
在沒有更多異步操作的時候,Node.js 會直接 0 狀態(tài)代碼退出。在其他情況下使用以下狀態(tài)代碼:
1 未捕獲的致命異常:存在未捕獲的異常,并且其沒有被 domain 模塊或 'uncaughtException' 事件處理器處理。
// 省略...
6 空函數(shù)的內(nèi)部異常處理器:存在未捕獲的異常,然后內(nèi)部致命異常處理器由于不明原因設(shè)置為了 nullptr(空函數(shù)),即沒有辦法執(zhí)行默認(rèn)的致命異常處理。
7 內(nèi)部異常處理器運行時失?。捍嬖谖床东@的異常,并且內(nèi)部致命異常處理函數(shù)本身在嘗試處理時拋出錯誤。例如,如果 'uncaughtException' 或 domain.on('error') 處理器拋出錯誤,就會發(fā)生這種情況。
// 省略..
>128 信號退出:如果 Node.js 收到致命的信號,例如 SIGKILL 或 SIGHUP,則其退出碼將是 128 加上信號代碼的值。這是標(biāo)準(zhǔn)的 POSIX 實踐,因為退出碼被定義為 7 位整數(shù),并且信號退出設(shè)置高位,然后包含信號代碼的值。例如,信號 SIGABRT 的值是 6,因此預(yù)期的退出碼將是 128 + 6 或 134。
完整參見 https://nodejs.org/api/process.html#process_exit_codes。
我們來舉個例子:
// arr-crash.jsprocess.on('uncaughtException', () => {console.error('Ya!');});const arr = [];while(true) arr.push(1);
然后測試:

在 Node.js 進程 crash 之后我們通過 echo $? 可以輸出之前進程崩潰之后的退出碼,上例中為 134。根據(jù)上文中的文檔,我們可以知道 134 = 128 + 6 即導(dǎo)致進程退出的異常類型為 6,參考文檔:
6 空函數(shù)的內(nèi)部異常處理器:存在未捕獲的異常,然后內(nèi)部致命異常處理器由于不明原因設(shè)置為了 nullptr(空函數(shù)),即沒有辦法執(zhí)行默認(rèn)的致命異常處理。
雖然無法根據(jù)錯誤碼知道本例中的進程 crash 的異常是什么,但是可以知道 crash 的時候進程是被強行 SIGKILL 或 SIGHUP 退出的,并且此時 V8 連默認(rèn)的異常處理器都用不了了(內(nèi)存爆了啥都做不了了)。
再來一個測試列子:
// uncaught-exception.jsprocess.on('uncaughtException', () => {throw new Error('there..')});throw new Error('here..')
測試情況:

根據(jù)錯誤碼,我們可以找到文檔:
7 內(nèi)部異常處理器運行時失?。捍嬖谖床东@的異常,并且內(nèi)部致命異常處理函數(shù)本身在嘗試處理時拋出錯誤。例如,如果 'uncaughtException' 或 domain.on('error') 處理器拋出錯誤,就會發(fā)生這種情況。
可以發(fā)現(xiàn)跟我們測試的情況吻合 —— 在執(zhí)行致命異常處理器(uncaughtException handler)的時候拋錯導(dǎo)致異常無法處理然后進程退出。
廢棄 API 警告
Node.js 提供了官方的廢棄(deprecate)標(biāo)記,開發(fā)者也可以通過 util.deprecate 方法,將某些接口標(biāo)記為廢棄狀態(tài),例如:
const util = require('util');exports.mergeData = util.deprecate(() => {// 之前的代碼}, 'mergeData() 已廢棄. 請使用 merge() ');
我們也可以通過 --no-deprecations 來關(guān)閉平常使用過程中碰到的 deprecate 警告(不過不是很推薦)。
有時候我們想找到拋這個警告的代碼位置在哪,那么可以使用 --trace-deprecation 來開啟 trace 錯誤棧,這樣在警告出來的時候就會附上具體的代碼 stack。如果更嚴(yán)格一些不希望代碼中出現(xiàn)使用廢棄版本的情況,那么可以考慮使用 --throw-deprecations 標(biāo)志,這樣使用廢棄 API 的地方會直接 throw error,例如:

、
識別同步 I/O 操作
由于 Node.js 是單線程的,在使用提供服務(wù)的時候,如果出現(xiàn)了耗時過長的同步 I/O 操作,那么期間就會 block 住整個線程。為了避免這種情況我們可以使用 --trace-sync-io 標(biāo)志開啟 Node.js 內(nèi)置的同步 I/O 追蹤檢測功能。
const { readFileSync } = require('fs');setImmediate(() => readFileSync(__filename));
測試效果:

Unhandled Promise Rejection
測試代碼:
const p = new Promise((resolve, reject) => {// throw new Error(111); // 與下一行等效reject(new Error(111));});
測試效果
(node:10142) UnhandledPromiseRejectionWarning: Error: 111
at /Users/lellansinhuang/workspace/midwayjs-tutorial/tmp/reject.js:4:10
at new Promise (<anonymous>)
at Object.<anonymous> (/Users/lellansinhuang/workspace/midwayjs-tutorial/tmp/reject.js:2:11)
at Module._compile (internal/modules/cjs/loader.js:1147:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1167:10)
at Module.load (internal/modules/cjs/loader.js:996:32)
at Function.Module._load (internal/modules/cjs/loader.js:896:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)
at internal/main/run_main_module.js:17:47
(node:10142) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
JavaScript 的 Promise 有三種狀態(tài),分別是 pending、fulfilled 和 rejected。當(dāng)一個 promise 的 rejected 狀態(tài)沒有后續(xù)處理的時候就會觸發(fā)上述報錯。也就是說如果上面的代碼加上,如 p.catch(console.log) 這樣的捕獲異常邏輯就不會出現(xiàn) UnhandledPromiseRejectionWarning 警告。
Unhandled Promise Rejection 是在 Node.js v12 時引入的新特性。不處理 Promies 的這種狀態(tài)在瀏覽器中一種可以接受的行文,但是在服務(wù)端則不然,因為這種報錯可能導(dǎo)致內(nèi)存泄漏。為了避免這種問題,可以了解一下 --unhandled-rejections 的幾種設(shè)置:
throw: 觸發(fā)一個 unhandledRejection 事件,你可以通過
promise.on('unhandledRejection')來監(jiān)聽,如果你沒有監(jiān)聽,那么會當(dāng)成一個未捕獲的 error 拋出。(默認(rèn)行為)strict: 直接拋 error。
warn: 不論是否設(shè)置了監(jiān)聽行為,總是產(chǎn)生一個警告。
warn-with-error-code: 觸發(fā) unhandledRejection 事件,如果沒有監(jiān)聽,觸發(fā)一個警告,并把進程退出碼設(shè)置為 1。
none: 靜默所有警告。
我們將上文中的測試改為使用 strict 模式,則可以得到如下報錯:
$ node --unhandled-rejections=strict reject.js
/Users/lellansinhuang/workspace/midwayjs-tutorial/tmp/reject.js:4
reject(new Error(111));
^
Error: 111
at /Users/lellansinhuang/workspace/midwayjs-tutorial/tmp/reject.js:4:10
at new Promise (<anonymous>)
at Object.<anonymous> (/Users/lellansinhuang/workspace/midwayjs-tutorial/tmp/reject.js:2:11)
at Module._compile (internal/modules/cjs/loader.js:1147:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1167:10)
at Module.load (internal/modules/cjs/loader.js:996:32)
at Function.Module._load (internal/modules/cjs/loader.js:896:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:71:12)
at internal/main/run_main_module.js:17:47
通過 strict 讓進程直接 crash 或者流程報錯 block 住,這是一種 let it crash 的思想。
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學(xué)習(xí)感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
“分享、點贊、在看” 支持一波??
