[译]在 Node.js 中监测内存泄露

偶然看到这篇关于node.js内存泄露监测的文章,自己对node.js对性能调试并不了解,所以翻译一下这篇感觉还不错我文章以增强自己对印象。
原文地址:http://www.nearform.com/nodecrunch/self-detect-memory-leak-node

在node.js里面追踪内存泄露并不是一件容易对事情。下面讨论一下通过借助“memwatch”(支持 node.js 版本:0.10.x), “heapdump”这两个模块,怎样在node.js程序内部让它自己追踪内存泄露。

内存泄露插图

首先,写一个简单的内存泄露案例:

1
2
3
4
5
6
7
8
9
10
11
12
var http = require('http');

var server = http.createServer(function (req, res) {
for (var i = 0; i < 1000; i++) {
server.on('request', function leakfunc() {});
}

res.end('Hello World\n');
}).listen(1333, '127.0.0.1');

server.setMaxListeners(0);
console.log('Server running at http://127.0.0.1:1333/. Progress PID:', process.pid);

每个请求都额外的创建了1000个监听器。

如果我们在shell中通过 while true; do curl http://127.0.0.1:1333/; done 命令,不断地发起请求,然后再另外一个shell窗口通过 top -pid <process pid> 可以看到 node 进程占用非常高的内存使用率,而且非常不稳定。

我们的 node 进程已经疯了。那么我们怎么诊断出问题来呢?

内存泄露监测

memwatch” 模块是最好的内存泄露检查工具。首先,我们通过下面的命令安装这个模块:

1
npm install --save memwatch

然后,在我们的代码里调用它:

1
2
var memwatch = require('memwatch');
memwatch.setup();

同时,也要监听”leak”事件:

1
2
3
memwatch.on('leak', function(info) {
console.error('Memory leak detected: ', info);
});

现在当你再次运行上面的测试案例,我们可以在控制台看到下面的信息:

1
2
3
4
5
6
{
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'
}

(注:由于我自己的 node.js 版本问题,导致无法安装 “memwatch“, 所以这个数据直接来源于原文)
memwatch 监测到了内存泄露!它定义内存泄露的标准为:

一个内存泄露事件会在堆中增加5个连续的垃圾回收行为时触发

查看 memwatch 了解更多。

内存泄露分析

现在我们已经发现了内存泄露,那么下一步我们在做的就是分析造成内存泄露的原因所在!
打个比方,我们可以在”leak”事件发生时把堆信息dump出来看看:

1
2
3
4
5
6
7
8
9
10
11
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;
}
});

这样我可以拿到下面的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{ 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' },
...

我们可以在两次 leak 事件看出堆增长了 2.47MB 的大小,然后是由闭包造成的。注意如果你指明了函数,那在这里有可能看到详细的造成内存泄露的函数名,这样我们就可以更方便的调试。

然后,在上面的例子中,我们只知道是有闭包造成的内存泄露,这并没有太大的作用。

所以,我们还要借助 heapdump

heapdump

heapdump 可以把 V8 中的堆 dump 出来,然后你可以借助 Chrome 开发者工具进行分析。你可以在开发者工具中比较两个堆,这有助于你更好的定位内存泄露的原因。

下面我们开始在代码中使用 heapdump,我们在每一次 “leak” 事件被触发时,保存一份快照到硬盘上:

1
2
3
4
5
6
7
8
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);
});
});

这样,我们再次运行测试案例的时候,我们可以看到 /tmp 目录下多了一些 .heapsnapshot 文件。我们打开 Chrome 的开发者工具,选中 Profile 面板,然后导入我们的快照。

我们现在可以清晰的看见罪魁祸首是 leakyfunc()
堆快照
同时,我们页可以比较两个快照。这样更容易在两个时间点上找到内存泄露:
内存泄露快照比较