如何自己檢查NodeJS的代碼是否存在內存泄漏

如何自己檢查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)支持的版本在這個分支
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章