这篇文章是来自 Mozilla 的
身份团队的 A Node.JS Holiday Season 系列的第一篇,该团队上个月发布了 Persona 的第一个 Beta 版。为了制作 Persona,我们构建了一套工具,涵盖了从调试到本地化再到依赖项管理等各个方面。这个系列的文章将与社区分享我们的经验和工具,这些工具与任何使用 Node.JS 构建高可用性服务的人都有关。我们希望您喜欢这个系列,并期待您的想法和贡献。我们将从一个关于 Node.js 问题的细节开始:内存泄漏。我们介绍了 node-memwatch – 一个帮助发现和隔离 Node 中内存泄漏的库。
为什么要费心?
关于追踪内存泄漏,一个合理的问题是“为什么要费心?”。难道总会有更紧迫的问题需要优先解决吗?为什么不定期重启服务,或者给它增加更多内存呢?为了回答这些问题,我们建议三点
- 您可能不担心内存占用不断增加,但 V8 会担心。(V8 是 Node 运行的引擎。)随着泄漏的增长,V8 会越来越积极地进行垃圾回收,从而降低应用程序的速度。因此,在 Node 中,内存泄漏会影响性能。
- 泄漏可能会引发其他类型的故障。泄漏的代码可能会持有对有限资源的引用。您可能会用完文件描述符;您可能会突然无法打开新的数据库连接。此类问题可能在应用程序耗尽内存之前就出现,并且仍然会让您陷入困境。
- 最后,迟早,您的应用程序会崩溃。而且您可以肯定,这会发生在您变得流行的那一刻。然后每个人都会嘲笑您,并在 Hacker News 上说您坏话,您会很难过。
那滴水声是从哪里来的?
在一个复杂的应用程序的管道中,泄漏可能发生在各个地方。闭包可能是最著名和臭名昭著的。由于闭包会维护对其作用域中事物的引用,因此它们是泄漏的常见来源。
如果有人在寻找闭包泄漏,最终可能会发现它们,但在 Node 的异步世界中,我们以回调的形式一直生成闭包。如果这些回调的处理速度赶不上创建速度,内存分配就会累积起来,看起来不像是泄漏的代码也会表现出泄漏的现象。这更难发现。
您的应用程序也可能由于上游代码中的错误而泄漏。您可能能够追踪到泄漏源头的代码位置,但您也可能只是茫然地盯着自己写得完美的代码,想知道它怎么可能泄漏!
正是这些难以发现的泄漏让我们想要一个像 node-memwatch
这样的工具。传说几个月前,我们的 Lloyd Hilaiel 将自己关在一个壁橱里两天,试图追踪一个在重载测试下变得明显的内存泄漏。(顺便说一句,期待 Lloyd 即将发表的关于负载测试的文章。)
经过两天的二分查找,他发现罪魁祸首是在 Node 核心:http.ClientRequest
中的事件监听器没有被清理。(当 Node 最终修复此问题时,补丁包含了 两个微妙但至关重要的字符。)正是这段糟糕的经历让 Lloyd 想编写一个工具来帮助查找泄漏。
查找泄漏的工具
已经有一套良好且不断增长的工具集合,用于查找 Node.js 应用程序中的泄漏。以下是一些:
- Jimb Esser 的 node-mtrace,它使用
GCCmtrace
实用程序来分析堆使用情况。 - Dave Pacheco 的 node-heap-dump 会拍摄 V8 堆的快照,并将整个快照序列化到一个巨大的 JSON 文件中。它包括用于遍历和调查的工具
生成的 JavaScript 快照。 - Danny Coates 的 v8-profiler 和 node-inspector 为 V8 分析器提供了 Node 绑定,并使用 WebKit Web Inspector 提供了 Node 调试接口。
- Felix Gnass 对同一项目的 fork,取消禁用保留者图
- Felix Geisendörfer 的 Node 内存泄漏教程 简明扼要地解释了如何使用
v8-profiler
和node-debugger
,并且目前是大多数 Node.js 内存泄漏调试的最佳实践。 - Joyent 的 SmartOS 平台,它为您提供了用于 调试 Node.js 内存泄漏 的工具库。
我们喜欢所有这些工具,但没有一个完全适合我们的环境。Web Inspector 方法对于开发中的应用程序来说非常棒,但在实时部署中难以使用,尤其是在混合了多个服务器和子进程的情况下。因此,可能难以重现那些在长时间运行和负载很重的生产环境中发生的内存泄漏。像 dtrace
和 libumem
这样的工具确实令人惊叹,但并非在所有操作系统上都能工作。
进入 node-memwatch
我们想要一个平台无关的调试库,不需要任何检测就能告诉我们程序何时可能发生内存泄漏,并帮助我们找到泄漏的位置。因此,我们编写了 node-memwatch
。
它为您提供了三样东西
-
一个
'leak'
事件发射器memwatch.on('leak', function(info) { // look at info to find out about what might be leaking });
-
一个
'stats'
事件发射器var memwatch = require('memwatch'); memwatch.on('stats', function(stats) { // do something with post-gc memory usage stats });
-
一个堆差异类
var hd = new memwatch.HeapDiff(); // your code here ... var diff = hd.end();
-
还有一个用于触发垃圾回收的功能,这在
测试中很有用。好吧,四样东西。var stats = memwatch.gc();
memwatch.on('stats', ...)
:后 GC 堆统计信息
node-memwatch
可以在完整垃圾回收和内存压缩之后,在分配任何新的 JS 对象之前,直接发出内存使用情况样本。(它使用 V8 的后 GC 钩子 V8::AddGCEpilogueCallback
,在每次 GC 发生时收集堆使用情况统计信息。)
统计数据包括
usage_trend
current_base
estimated_base
num_full_gc
num_inc_gc
heap_compactions
min
max
这是一个示例,它显示了随着时间的推移,具有内存泄漏的应用程序的数据外观。下面的图表显示了随时间推移的内存使用情况。绿色曲线显示了 process.memoryUsage()
报告的内容,红色曲线显示了 node_memwatch
报告的 current_base
。左下角的方框显示了其他统计信息。
请注意,增量 GC 的数量非常高。这是一个警告信号,表明 V8 正在加班加点地尝试清理分配。
memwatch.on('leak', ...)
:堆分配趋势
我们有一个简单的启发式方法来警告您应用程序可能存在泄漏。如果在五个连续的 GC 中,您继续分配内存而没有释放它,node-memwatch
将发出 leak
事件。该消息以人性化的形式告诉您发生了什么
{ start: Fri, 29 Jun 2012 14:12:13 GMT,
end: Fri, 29 Jun 2012 14:12:33 GMT,
growth: 67984,
reason: 'heap growth over 5 consecutive GCs (20s) - 11.67 mb/hr' }
memwatch.HeapDiff()
:查找泄漏
最后,node-memwatch
可以比较堆上对象名称和分配计数的快照。生成的差异可以帮助隔离违规者。
var hd = new memwatch.HeapDiff();
// Your code here ...
var diff = hd.end();
diff
的内容如下所示
{
"before": {
"nodes": 11625,
"size_bytes": 1869904,
"size": "1.78 mb"
},
"after": {
"nodes": 21435,
"size_bytes": 2119136,
"size": "2.02 mb"
},
"change": {
"size_bytes": 249232,
"size": "243.39 kb",
"freed_nodes": 197,
"allocated_nodes": 10007,
"details": [
{
"what": "Array",
"size_bytes": 66688,
"size": "65.13 kb",
"+": 4,
"-": 78
},
{
"what": "Code",
"size_bytes": -55296,
"size": "-54 kb",
"+": 1,
"-": 57
},
{
"what": "LeakingClass",
"size_bytes": 239952,
"size": "234.33 kb",
"+": 9998,
"-": 0
},
{
"what": "String",
"size_bytes": -2120,
"size": "-2.07 kb",
"+": 3,
"-": 62
}
]
}
}
HeapDiff
在获取样本之前会触发完整 GC,因此数据中不会包含很多垃圾。memwatch
的事件发射器不会通知 HeapDiff
GC 事件,因此您可以安全地将 HeapDiff
调用放入 'stats'
处理程序中。
在下图中,我们添加了堆分配最多的对象
下一步
node-memwatch
提供
- 准确的内存使用情况跟踪
- 关于可能泄漏的通知
- 生成堆差异的方法
- 它是跨平台的
- 并且不需要任何额外的检测
我们希望它能做更多的事情。特别是,我们希望 node-memwatch
能够提供一些泄漏对象的示例(例如,变量名称、数组索引或闭包代码)。
我们希望您发现 node-memwatch
对调试 Node 应用程序中的泄漏很有用,并且您会 fork 代码并帮助我们改进它。
19 条评论