鬼影追踪 —— 发现 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会保持一个到这个对象本身的引用,以防止垃圾回收器释放其资源。
底层工具mdbmdb 工具是一个用于对操作系统,系统故障转储,用户进程,进程信息转储和对象文件底层的调试和编辑的可扩展工具。
gcore生成正在运行中的程序的信息转储,包括进程 ID pid。
放在一起首先我们需要创建一个转储用来研究。你可以简单地实现:
gcore `pgrep node`在你获得之后,你可以通过下面的命令搜索堆中全部的 JS 对象:
> ::findjsobjects当然,你需要获取连续的信息转储来比较转储间的不同。
一旦你发现了可疑的对象,你可以这样分析它们:
object_id::jsprint现在你所需要做的便是寻找对象(根节点)的持有者(retainer)。
object_id::findjsobjects -r这个命令将会返回持有者的 id。然后你可以再次使用 ::jsprint 来分析这些持有者。
你可以通过观看来自 Netflix 的 Yunong Xiao 的讲座来了解关于如何使用它的更详细的版本。
分享视频地址(YouTube,需自备墙梯)
推荐阅读http://www.codeceo.com/article/nodejs-memory-leak.html
鬼影追踪 —— 发现 Node.js 中的内存泄漏
|