鬼影追踪 —— 发现 Node.js 中的内存泄漏

本文由码农网 – 任琦磊原创翻译,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划

发现 Node.js 的内存泄露可能是一个不小的挑战 —— 最近我们就有这样一个经验可以拿来分享。

我们客户的一个微服务产生了如下图所示的内存使用情况:

通过 Trace by RisingStack(一款 Node.js 性能监控和调试工具)抓取到的内存使用情况

你可能会花费几天的时间在这类东西上:剖析应用来查找根源问题。本文中,我会来总结一下你能够使用什么工具以及如何使用他们,所以希望你能从中有所收获。

“太长,勿看(TL;DR)”版本

在我们这个特定的情况中,服务正在一台仅有 512MB 的小实例上运行。事实上,应用并没有泄露任何内存,而是 GC 甚至没有开始回收已取消引用的对象。

这为什么会发生呢? 默认地,Node.js 会尝试使用大约 1.5GB 的内存。因此当在内存比这小的系统上运行时,很有必要覆写该内存使用默认值,因为垃圾回收是一个很消耗资源的操作。

它的解决方案是在 Node.js 运行时添加一个额外的参数:

node --max_old_space_size=400 server.js --production

可是,如果情况没有像上例这样明显的话,你又如何来发现内存泄漏的问题呢?

理解 V8 的内存处理

在我们深入研究你能够用来寻找和修复 Node.js 应用中内存泄漏问题的技术前,让我们先来看一下 V8 是如何处理内存的。

定义

  • 常住集大小(resident set size): 是内存中被进程占用并保留的 RAM 部分,包括:
    • 代码本身
  • 栈(stack): 包含基本类型(primitive types)和对象的引用(references to objects)
  • 堆(heap): 存储引用类型(reference types),如对象,字符串或闭包
  • 对象的直接占用内存(shallow size of an object): 对象本身自己直接占用的内存空间
  • 对象的占用总内存(retained size of an object): 当对象与其关联对象一起被删除时释放出的内存空间

垃圾回收器是如何工作的

垃圾回收是将应用已不再使用的对象占用的内存进行回收的过程。通常来说,内存的分配相当容易,但当内存池(memory pool)已经耗尽需要回收内存时却相当困难。

当根节点不可达某个对象时,它便进入了垃圾回收的候选名单了,所以不要被根对象或其它任意有效对象引用。根对象可以是全局对象,DOM 元素或局部变量。

堆两个主要的区段,新生代空间(New Space)和老生代空间(Old Space)。新生代空间用于新的内存分配,一般在约为 1-8MB 左右,所以这里的垃圾回收很快。在新生代空间中的对象被称为新生代(Young Generation)。老生代空间则存放那些免于回收从新生代空间晋升至此的对象——它们被称为老生代(Old Generation)。老生代空间分配内存很方便但回收却很困难,所以垃圾回收很少在这里执行。

垃圾回收为什么会变得如此困难? V8 JavaScript 引擎采用了“停止一切(stop-the-world)”垃圾回收器机制。实际使用中,这意味着垃圾回收处理过程中程序会停止执行。

通常,约 20% 的新生代会留下来进入老生代。只有到了内存耗尽的时候才会开始回收老生代空间的内存。这些 V8 引擎是通过使用两种不同的回收算法来实现的:

  • Scavenge 回收,快速且运行在新生代的回收上。
  • Mark-Sweep 回收,较慢且运行在老生代的回收上。

更多关于这如何工作的信息可以参考文章 V8 之旅:垃圾回收。更过关于整体内存管理的信息,可访问内存管理参考

寻找 Node.js 内存泄漏时你可以使用的工具/技术

heapdump 模块

你可以通过使用 heapdump 模块来创建一个堆的快照以便日后检查。把它添加到你的项目很简单:

npm install heapdump --save

然后在你的进入点(entry point)只要添加:

var heapdump = require('heapdump');

当你完成了上述操作,你就可以开始收集 heapdump 了,你可以通过使用命令 $ kill -USR2 <pid>,或者通过调用:

heapdump.writeSnapshot(function(err, filename) {  
  console.log('dump written to', filename);
});

一旦你获取了你的快照,就是时候让它们发光发热了。你最好确保你捕获了不同时间的多个快照,这样你就可以将它们进行比较了。

谷歌 Chome 开发者工具

首先你需要将你的内存快照加载进 Chrome 分析器。方法为:打开 Chrome 开发者工具,进入 Profiles,然后 加载 你的堆快照。

当你完成加载后,它应该看起来像这样:

到目前为止一切正常,但这个截屏里到底能看出些什么东西呢?

这里需要注意的一个重要的事情是已选中的视图窗口:Comparison。这个模式允许你来比较两个(或多个)不同时间获取的堆快照,所以你能够准确地找出哪些对象分配到了内存,与此同时哪个的没有释放。

另一个重要的标签是 Retainers。它用来展示到底为什么一个对象不可以被垃圾回收掉,是什么仍然引用着它。这种情况下,全局变量 log会保持一个到这个对象本身的引用,以防止垃圾回收器释放其资源。

底层工具

mdb

mdb 工具是一个用于对操作系统,系统故障转储,用户进程,进程信息转储和对象文件底层的调试和编辑的可扩展工具。

gcore

生成正在运行中的程序的信息转储,包括进程 ID pid。

放在一起

首先我们需要创建一个转储用来研究。你可以简单地实现:

gcore `pgrep node`

在你获得之后,你可以通过下面的命令搜索堆中全部的 JS 对象:

> ::findjsobjects

当然,你需要获取连续的信息转储来比较转储间的不同。

一旦你发现了可疑的对象,你可以这样分析它们:

object_id::jsprint

现在你所需要做的便是寻找对象(根节点)的持有者(retainer)。

object_id::findjsobjects -r

这个命令将会返回持有者的 id。然后你可以再次使用 ::jsprint 来分析这些持有者。

你可以通过观看来自 Netflix 的 Yunong Xiao 的讲座来了解关于如何使用它的更详细的版本。

分享视频地址(YouTube,需自备墙梯)

推荐阅读

你有关于 Node.js 内存泄漏更多的想法或见解吗?在评论中分享一下吧。

译文链接:http://www.codeceo.com/article/nodejs-memory-leak.html
英文原文:Hunting a Ghost - Finding a Memory Leak in Node.js
翻译作者:码农网 – 任琦磊
转载必须在正文中标注并保留原文链接、译文链接和译者等信息。]

相关文章

在文章中找不到问题答案?您还可以

前往问答社区提问

关注我们的微博

付费投稿计划
点击查看详情