如何對Node應用"死後驗屍"

scq000發表於2019-11-28

為了讓前端工作更有效率,必須徹底掌握一些必要的除錯技巧。平常在開發Node應用的過程中,最常使用的是本地除錯,但是一旦你的程式碼到了生產環境,就必須採取其他策略進行追蹤和解決問題。

在程式設計領域,有一個專門的術語“Post-mortem debugging",它的意思是在程式奔潰後再進行的除錯工作。對於Node程式來說,如果你遇到了難以重現、線上環境無法除錯等問題都可以採用這種方案進行操作。而且線上問題一般都是比較緊急的,所以我們一般都希望能最快定位到問題發生的程式碼。

那麼我們具體要怎麼做呢?下面舉個簡單的例子。

收集奔潰資訊

假設現在你有這樣一段程式碼:

const demo = (data) => {
        const {id, profile} = person;
        console.log(id);
        console.log(profile.name);
}

demo({id: 1, profile: {age: 12}});
複製程式碼

執行後它會報錯並退出。這時候你需要做的是收集奔潰資訊並對其做分析。Core Dump就是這樣用來記錄程式執行資訊的一種工具,它包含了程式執行過程中的記憶體狀態,呼叫棧等,能最真實地還原當時的“案發現場“。

那麼,在Node.js中我們怎麼獲得Core Dump的檔案呢?

首先,我們先設定一下系統中的核心限制:

ulimit -c unlimited
複製程式碼

然後你需要在啟動應用的時候,使用--abort-on-uncaught-exception這個flag來手動觸發程式奔潰後寫core檔案的操作:

node --abort-on-uncaught-exception app.js
複製程式碼

draggingScreenshot.png

這樣當程式突然奔潰的時候,就會在linux或mac系統的/cores目錄下生成類似core.81371這樣的一個檔案。這個檔案就是我們用來除錯調查程式奔潰的核心。

如果你的程式正在執行過程中,我們也可以手動捕獲core dump檔案,類似於實時檢查,主要用於程式假死等狀態。

手動捕獲的話需要使用Linux系統自帶的 gcore 命令,具體用法是找出當前程式的pid(這裡假設是123),然後執行命令:

gcore 123
複製程式碼

生成對應的core dump檔案。

另外一種方式是採用lldb除錯工具,mac系統下使用該命令進行安裝:

brew install --with-lldb --with-toolchain llvm
複製程式碼

然後執行:

lldb --attach-pid <pid> -b -o 'process save-core' "core.<pid>"'
複製程式碼

這樣就能在不重啟程式的情況下匯出特定程式的core dump檔案。

除錯步驟

得到具體的core dump檔案後,我們就要進入除錯分析階段了。

首先,需要使用選擇順手的分析工具。你可以選擇mdb_v8或者llnode。這兩個工具用起來都差不多。

這裡以llnode為例,先介紹幾個常用命令:

命令 意義
v8 help 檢視幫助資訊
v8 bt get stack trace at crash 檢視堆疊資訊
v8 souce list 顯示stack frame的原始碼
v8 inspect 檢視對應地址的物件內容
frame select 選擇對應的stack frame

在分析前,先需要用llnode載入core檔案:

llnode -c /cores/core.81371
複製程式碼

然後獲取對應的堆疊資訊:

// 檢視堆疊資訊
(llnode) v8 bt
// 根據堆疊資訊找到可疑的地址,並檢視對應的物件內容
(llnode) v8 inspect <address>
// 指定對應的stack frame
(llnode) frame select 6
// 檢視原始碼
(llnode) v8 source list
複製程式碼

最後通過結合堆疊資訊和原始碼就能找到錯誤發生的原因了。

記憶體洩漏

除了程式奔潰,有時候你還會發現應用隨著執行時間增長,速度開始變慢。這可能就是記憶體洩漏搗的鬼。

比如下面這段程式碼:

const requests = new Map();

app.get("/", (req, res) => {
  requests.set(req.id, req);
  res.status(200).send("hello")
})
複製程式碼

通常來說,記憶體洩漏容易發生在閉包等場景下。針對記憶體洩漏的除錯,可以使用如下命令:

node --trace_gc --trace_gc_verbose app.js
複製程式碼

啟動應用後,通過壓測工具執行如下命令:

ab -k -c200 -n10000000 http://localhost:3000
複製程式碼

draggingScreenshot.png

可以看到隨著程式的執行,記憶體使用越來越大。

另外,我們還可以使用heap snapshot來獲取快照資訊:

process.on('SIGUSR2', () => {
	const { writeHeapSnapshot } = require("v8");
	
	console.log("Heap snapshot has written:", writeHeapSnapshot())
})
複製程式碼

在命令列中執行:

kill -SIGUSR2 <pid>
複製程式碼

就能夠獲得對應的快照檔案,然後我們可以使用Chrome Devtools的Memory選單載入對應的快照檔案進行比對分析了。

如果是開發階段,你也可以直接使用除錯模式啟動應用:

node --inspect app.js
複製程式碼

然後使用選單Devtools > Memory > take heap snapshot獲得快照檔案。

draggingScreenshot.png

通過比較兩個不同的記憶體快照,我們可以很快找到記憶體增長最快的那個物件,然後進而分析對應的原始碼就能知道問題出在了哪裡。

除了採用Chrome瀏覽器,在linux主機上,我們還能使用萬能的llnode偵錯程式進行記憶體洩漏的分析。原理大致相同,也是通過分析core 檔案,然後安裝物件大小排序,針對可疑的物件進行原始碼檢視。

(llnode) v8 findjsobjects
(llnode) v8 findjsinstances -d <Object>
(llnode) v8 inspect -m <address》
(llnode) v8 findrefs <address>
複製程式碼

其它策略

另外一種收集報告的策略是使用引數,適用於13.0以上版本:

node \
	--experimental-report \
	--diagnostic-report-uncaught-exception \
	--diagnostic-report-on-fatalerror \
	app.js
複製程式碼

這樣在程式奔潰的時候,就能夠獲取到對應的報告。你還可以通過程式碼顯式控制報告的輸出檔名等:

process.report.writeReport('./foo.json');

複製程式碼

更多說明可以參考官方文件: nodejs.org/api/report.…

——轉載請註明出處———

微信掃描二維碼,關注我的公眾號

如何對Node應用"死後驗屍"

最後,歡迎大家關注我的公眾號,一起學習交流。

參考資料

medium.com/netflix-tec… en.wikipedia.org/wiki/Debugg… www.bookstack.cn/read/node-i… github.com/bnoordhuis/…

相關文章