如何自己檢查NodeJS的代碼是否存在內存泄漏
追蹤NodeJS代碼中的內存泄漏一直是一個很有挑戰的難題。本文討論如何從一個node寫的應用裏自動的跟蹤到內存泄漏問題,在這裏筆者向大家推薦兩款追查內存問題的神器 —— memwatch 和 heapdump
首先,我們來看一個簡單的內存泄漏
var http = require('http');
var server = http.createServer(function (req, res) {
for (var i=0; i<1000; i++) {
server.on('request', function leakyfunc() {});
}
res.end('Hello World\n');
}).listen(1337, '127.0.0.1');
server.setMaxListeners(0);
console.log('Server running at http://127.0.0.1:1337/. Process PID: ', process.pid);
每一個請求我們增加了1000個導致泄漏的監聽器。如果我們在一個shell控制檯中執行以下命令:
while true; do curl http://127.0.0.1:1337/; done
然後在另外一個shell控制檯中查看我們的進程
top -pid
內存泄露的檢測
npm模塊 memwatch 是一個非常好的內存泄漏檢查工具,讓我們先將這個模塊安裝到我們的app中去,執行以下命令:
npm install --save memwatch
然後,在我們的代碼中,添加:
var memwatch = require('memwatch');
memwatch.setup();
然後監聽 leak 事件
memwatch.on('leak', function(info) {
console.error('Memory leak detected: ', info);
});
這樣當我們執行我們的測試代碼,我們會看到下面的信息:
{
start: Fri Jan 02 2015 10:38:49 GMT+0000 (GMT),
end: Fri Jan 02 2015 10:38:50 GMT+0000 (GMT),
growth: 7620560,
reason: 'heap growth over 5 consecutive GCs (1s) - -2147483648 bytes/hr'
}
memwatch發現了內存泄漏!memwatch 判定內存泄漏事件發生的規則如下:
當你的堆內存在5個連續的垃圾回收週期內保持持續增長,那麼一個內存泄漏事件被派發
內存泄漏分析
使用memwatch我們發現了存在內存泄漏,這非常好,但是現在呢?我們還需要定位內存泄漏出現的實際位置。要做到這一點,有兩種方法可以使用。
memwatch heap diff
通過memwatch你可以得到堆內存使用量和內存隨程序運行產生的差異。
例如,我們可以在兩個leak事件發生的間隔中做一個heap dump
:
var hd;
memwatch.on('leak', function(info) {
console.error(info);
if (!hd) {
hd = new memwatch.HeapDiff();
} else {
var diff = hd.end();
console.error(util.inspect(diff, true, null));
hd = null;
}
});
執行這段代碼會輸出更多的信息:
{ before: {
nodes: 244023,
time: Fri Jan 02 2015 12:13:11 GMT+0000 (GMT),
size_bytes: 22095800,
size: '21.07 mb' },
after: {
nodes: 280028,
time: Fri Jan 02 2015 12:13:13 GMT+0000 (GMT),
size_bytes: 24689216,
size: '23.55 mb' },
change: {
size_bytes: 2593416,
size: '2.47 mb',
freed_nodes: 388,
allocated_nodes: 36393,
details:
[ { size_bytes: 0,
'+': 0,
what: '(Relocatable)',
'-': 1,
size: '0 bytes' },
{ size_bytes: 0,
'+': 1,
what: 'Arguments',
'-': 1,
size: '0 bytes' },
{ size_bytes: 2856,
'+': 223,
what: 'Array',
'-': 201,
size: '2.79 kb' },
{ size_bytes: 2590272,
'+': 35987,
what: 'Closure',
'-': 11,
size: '2.47 mb' },
...
所以在內存泄漏事件之間,我們發現堆內存增長了2.47MB,而導致內存增長的罪魁禍首是閉包,如果你的泄漏是由某個class造成的,那麼what
字段可能會輸出具體的class名字,所以這樣的話,你會獲得足夠的信息來幫助你最終定位到泄漏之處。
然而,在我們的例子中,我們唯一獲得的信息只是泄漏來自於閉包,這個信息非常有用,但是仍不足以在一個複雜的應用中迅速找到問題的來源(複雜的應用往往有很多的閉包,不知道哪一個造成了內存泄漏)
所以我們該怎麼辦呢?這時候該Heapdump出場了。
Heapdump
npm模塊node-heapdump是一個非凡的模塊,它可以使用來將v8引擎的堆內存內容dump出來,這樣你就可以在Chrome的開發者工具中查看問題。你可以在開發工具中對比不同運行階段的堆內存快照,這樣可以幫助你定位到內存泄漏的位置。
現在讓我們來試試 heapdump,在每一次發現內存泄漏的時候,我們都將此時的內存堆棧快照寫入磁盤中:
memwatch.on('leak', function(info) {
console.error(info);
var file = '/tmp/myapp-' + process.pid + '-' + Date.now() + '.heapsnapshot';
heapdump.writeSnapshot(file, function(err){
if (err) console.error(err);
else console.error('Wrote snapshot: ' + file);
});
});
運行我們的代碼,磁盤上會產生一些.heapsnapshot
的文件到/tmp
目錄下。現在,在Chrome瀏覽器中,啓動開發者工具(在mac下的快捷鍵是alt+cmd+i),點擊Profiles
標籤並點擊Load
按鈕載入我們的快照。
我們能夠很清晰地發現原來leakyfunc()是內存泄漏的元兇。
我們依然還可以通過對比兩次記錄中heapdump的不同來更加迅速確認兩次dump之間的內存泄漏:
想要進一步瞭解開發者工具的memory profiling
功能,可以閱讀 Taming The Unicorn: Easing JavaScript Memory Profiling In Chrome DevTools 這篇文章。
Turbo Test Runner
我們給Turbo - FeedHenry開發的測試工具提交了一個小補丁 — 使用了上面所說的內存泄漏檢查技術。這樣就可以讓開發者寫針對內存的單元測試了,如果模塊有內存問題,那麼測試結果中就會產生相應的警告。詳細瞭解具體的內容,可以訪問Turbo模塊。
上面的內容討論了一種檢測NodeJS內存泄漏的基本方法,以下是一些結論:
- heapdump有一些潛規則,例如快照大小等。仔細閱讀說明文檔,並且生成快照也是比較消耗CPU資源的。
- 還有些其他方法也能生成快照,各有利弊,針對你的項目選擇最適合的方式。(例如,發送sigusr2到進程等等,這裏有一個memwatch-sigusr2項目)
- 需要考慮在什麼情況下開啓memwatch/heapdump。只有在測試環境中有開啓它們的必要,另外也需要考慮heapdump的頻度以免耗盡了CPU。總之,選擇最適合你項目的方式。
- 也可以考慮其他的方式來檢測內存的增長,比如直接監控process.memoryUsage()是一個可以考慮的方法。
- 當內存問題被探測到之後,你應該要確定這確實是個內存泄漏問題,然後再告知給相關人員。
- 當心誤判,短暫的內存使用峯值表現得很像是內存泄漏。如果你的app突然要佔用大量的CPU和內存,處理時間可能會跨越數個垃圾回收週期,那樣的話memwatch很有可能將之誤判爲內存泄漏。但是,這種情況下,一旦你的app使用完這些資源,內存消耗就會降回正常的水平。所以,你其實需要注意的是持續報告的內存泄漏,而可以忽略一兩次突發的警報。
- memwatch目前僅支持node 0.10.x,node 0.12.x(可能還有io.js)支持的版本在這個分支