追踪 Node.js 中的内存泄漏 – Node.JS 假期季

这篇文章是来自 Mozilla 的
身份团队的 A Node.JS Holiday Season 系列的第一篇,该团队上个月发布了 Persona 的第一个 Beta 版。为了制作 Persona,我们构建了一套工具,涵盖了从调试到本地化再到依赖项管理等各个方面。这个系列的文章将与社区分享我们的经验和工具,这些工具与任何使用 Node.JS 构建高可用性服务的人都有关。我们希望您喜欢这个系列,并期待您的想法和贡献。

我们将从一个关于 Node.js 问题的细节开始:内存泄漏。我们介绍了 node-memwatch – 一个帮助发现和隔离 Node 中内存泄漏的库。

为什么要费心?

关于追踪内存泄漏,一个合理的问题是“为什么要费心?”。难道总会有更紧迫的问题需要优先解决吗?为什么不定期重启服务,或者给它增加更多内存呢?为了回答这些问题,我们建议三点

  1. 您可能不担心内存占用不断增加,但 V8 会担心。(V8 是 Node 运行的引擎。)随着泄漏的增长,V8 会越来越积极地进行垃圾回收,从而降低应用程序的速度。因此,在 Node 中,内存泄漏会影响性能。
  2. 泄漏可能会引发其他类型的故障。泄漏的代码可能会持有对有限资源的引用。您可能会用完文件描述符;您可能会突然无法打开新的数据库连接。此类问题可能在应用程序耗尽内存之前就出现,并且仍然会让您陷入困境。
  3. 最后,迟早,您的应用程序会崩溃。而且您可以肯定,这会发生在您变得流行的那一刻。然后每个人都会嘲笑您,并在 Hacker News 上说您坏话,您会很难过。

那滴水声是从哪里来的?

在一个复杂的应用程序的管道中,泄漏可能发生在各个地方。闭包可能是最著名和臭名昭著的。由于闭包会维护对其作用域中事物的引用,因此它们是泄漏的常见来源。

如果有人在寻找闭包泄漏,最终可能会发现它们,但在 Node 的异步世界中,我们以回调的形式一直生成闭包。如果这些回调的处理速度赶不上创建速度,内存分配就会累积起来,看起来不像是泄漏的代码也会表现出泄漏的现象。这更难发现。

您的应用程序也可能由于上游代码中的错误而泄漏。您可能能够追踪到泄漏源头的代码位置,但您也可能只是茫然地盯着自己写得完美的代码,想知道它怎么可能泄漏!

正是这些难以发现的泄漏让我们想要一个像 node-memwatch 这样的工具。传说几个月前,我们的 Lloyd Hilaiel 将自己关在一个壁橱里两天,试图追踪一个在重载测试下变得明显的内存泄漏。(顺便说一句,期待 Lloyd 即将发表的关于负载测试的文章。)

经过两天的二分查找,他发现罪魁祸首是在 Node 核心:http.ClientRequest 中的事件监听器没有被清理。(当 Node 最终修复此问题时,补丁包含了 两个微妙但至关重要的字符。)正是这段糟糕的经历让 Lloyd 想编写一个工具来帮助查找泄漏。

查找泄漏的工具

已经有一套良好且不断增长的工具集合,用于查找 Node.js 应用程序中的泄漏。以下是一些:

  • Jimb Esser 的 node-mtrace,它使用
    GCC mtrace 实用程序来分析堆使用情况。
  • Dave Pacheco 的 node-heap-dump 会拍摄 V8 堆的快照,并将整个快照序列化到一个巨大的 JSON 文件中。它包括用于遍历和调查的工具
    生成的 JavaScript 快照。
  • Danny Coates 的 v8-profilernode-inspector 为 V8 分析器提供了 Node 绑定,并使用 WebKit Web Inspector 提供了 Node 调试接口。
  • Felix Gnass 对同一项目的 fork,取消禁用保留者图
  • Felix Geisendörfer 的 Node 内存泄漏教程 简明扼要地解释了如何使用 v8-profilernode-debugger,并且目前是大多数 Node.js 内存泄漏调试的最佳实践。
  • Joyent 的 SmartOS 平台,它为您提供了用于 调试 Node.js 内存泄漏 的工具库。

我们喜欢所有这些工具,但没有一个完全适合我们的环境。Web Inspector 方法对于开发中的应用程序来说非常棒,但在实时部署中难以使用,尤其是在混合了多个服务器和子进程的情况下。因此,可能难以重现那些在长时间运行和负载很重的生产环境中发生的内存泄漏。像 dtracelibumem 这样的工具确实令人惊叹,但并非在所有操作系统上都能工作。

进入 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。左下角的方框显示了其他统计信息。

leak-gc-events

请注意,增量 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' 处理程序中。

在下图中,我们添加了堆分配最多的对象

heap-allocations

下一步

node-memwatch 提供

  • 准确的内存使用情况跟踪
  • 关于可能泄漏的通知
  • 生成堆差异的方法
  • 它是跨平台的
  • 并且不需要任何额外的检测

我们希望它能做更多的事情。特别是,我们希望 node-memwatch 能够提供一些泄漏对象的示例(例如,变量名称、数组索引或闭包代码)。

我们希望您发现 node-memwatch 对调试 Node 应用程序中的泄漏很有用,并且您会 fork 代码并帮助我们改进它。


19 条评论

  1. Ron Waldon

    这看起来很棒。我迫不及待地想在我的 Node.JS 项目中试用它。

    2012 年 11 月 6 日 下午 3:05

    1. Robert Nyman [Mozilla]

      谢谢,很高兴您喜欢它!

      2012 年 11 月 7 日 上午 1:37

  2. nodejs-news

    您好,Mozilla,

    调试我的 node.js 代码的有用工具!我将立即使用它。

    谢谢

    2012 年 11 月 7 日 上午 3:26

  3. Nico

    感谢这个不错的工具!

    但是我遇到了不同的问题:堆看起来正常并且没有增长,但 rss 使用量会增长到一定限度(应用程序不会崩溃)。我在这里做了一些截图

    https://github.com/einaros/ws/issues/43

    我怀疑是缓冲区实现未能释放所有缓冲区内存,但这确实很难调试,因为没有一个工具可以检查非堆内存……

    2012 年 11 月 7 日 上午 7:57

  4. christoph

    这会引入开销吗?它适用于生产环境还是仅在开发期间使用?

    2012 年 11 月 9 日 上午 7:17

    1. Lloyd Hilaiel

      嗨,cristoph,

      除了 HeapDiff 功能外,它旨在用于生产环境 – 它仅在 V8 执行的 gc 之后运行,并努力快速执行计算(在 c++ 中)并且不分配任何明显的内存。

      但话虽如此,我们还没有在自己的 persona 服务中使用它,尽管我希望很快就能使用。我们的想法是注册“stats”事件并在监视器上触发 current_base(我们使用 statsd),这将为我们提供基础内存使用情况的实时图表……我们目前只监视 RSS。

      现在 HeapDiff 功能更新、更复杂、成本更高,并且仍在稳定中(请参阅刚刚发布的 v. 0.2.0)。虽然在 0.2.0 中我对它更有信心,但我仍会更加谨慎,并在将这部分内容引入高可用性生产系统之前进行彻底的负载测试。

      2012 年 11 月 10 日 下午 4:17

  5. Alexey Kupershtokh

    我想知道您是如何绘制这些图表的

    2012 年 11 月 19 日 下午 10:19

    1. Jed Parsons

      嗨,Alexey,

      那是使用 d3 (https://d3js.cn)。这些图像是使用以下代码从实时演示中截取的屏幕截图:https://github.com/jedp/node-memwatch-demo

      2012 年 11 月 20 日 上午 9:30

  6. Felix

    当 GC 发生时,绿色线不应该总是接触红色线吗?

    2012 年 11 月 23 日 下午 2:57

    1. Jed Parsons

      嗨,Felix,是的,可能;但它正在进行时间采样以获取这些线,因此有时您会得到绿色线接触红色线的位置,但大多数情况下不会。

      2012 年 12 月 7 日 上午 10:02

  7. Camilo Aguilar

    精彩的文章,我想知道你们是否尝试过 https://github.com/c4milo/node-webkit-agent。如果尝试过,我还想知道为什么它没有用。

    2012 年 11 月 25 日 下午 2:30

    1. Camilo Aguilar

      顺便说一句,Vincent,修复 nodejs 中泄漏的人,使用 node-webkit-agent 发现了它

      2012 年 11 月 25 日 下午 2:54

    2. Jed Parsons

      嗨,Camilo,

      我非常喜欢 node-webkit-agent,并且一直在使用它。它很棒,我不敢相信它没有出现在上面的列表中。这完全是我的错误,我将编辑这篇文章来修复它。

      我个人认为,Web Inspector 方法非常适合在本地机器上进行有针对性的挖掘和错误追踪,但不太适合监控分布式服务器及其可能产生的子进程的长期运行、实时部署。此外,我对于在生产代码中添加额外的服务器和钩子来连接外部调试器感到不适。

      但也许是我使用方式不对;你是否将其用于大型部署?我可以想象,如果让 node-memwatch 在后台运行并进行长期监控,可以从中获得很多好处;如果它检测到内存泄漏,它可以发送地址和端口号,以便你使用 node-webkit-agent 实时深入分析。这将非常强大。

      2012年12月7日 上午10:31

      1. Camilo Aguilar

        嘿,Jed!

        你说得对,node-memwatch 和 node-webkit-agent 似乎是一个强大的组合。

        为了更清楚地说明,node-webkit-agent 本身没有任何额外的检测机制,它使用 v8 内部分析器并通过 Webkit Devtools 提供了一个接口来操作它,仅此而已。它非常轻量级。但是,正如你指出的,它不像 node-memwatch 那样能够实时检测内存泄漏。它主要用于当有人注意到异常的内存行为并希望远程深入分析时。

        至于生产环境的使用,node-webkit-agent 是在 BugLabs 开发的,用于追踪生产环境中的内存泄漏,因为当时缺乏此类工具。我也听说过其他人也在生产部署中使用它,但我不确定他们的规模有多大。

        无论如何,node-memwatch 绝对是一个值得与 node-webkit-agent 结合使用的工具。干得好,伙计们。

        2012年12月12日 上午08:57

  8. sham singh

    很棒的文章。过去几天我一直在研究分析工具(node-inspector 似乎不再显示 profile 选项卡),以帮助追踪为什么我的 node 应用的 RSS 似乎在缓慢增长……node-memwatch 现在成为了我的工具箱中的一员。

    2012年12月5日 下午14:14

    1. Jed Parsons

      谢谢,Sham Singh。很高兴它对你有用!

      2012年12月7日 上午10:32

  9. Camilo Aguilar

    现在真正有用的工具是针对 nodejs 的,它允许我们在 v8 堆之外继续分析内存。我见过一些案例,其中持续增长的内存位于 v8 堆之外。

    2012年12月12日 上午09:05

  10. Dipesh Bhardwaj

    它真的很棒,node-memwatch 非常实用。我正在做一个非常大的项目,我肯定会在其中使用它。

    2013年1月31日 下午22:18

  11. txf

    真是太棒了,非常喜欢这个工具,迫切需要它来帮助构建健壮的应用程序

    2013年3月7日 上午01:26

本文评论已关闭。