如何调试 Node.js的内存泄露
Posted 前端大全
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何调试 Node.js的内存泄露相关的知识,希望对你有一定的参考价值。
原文:Hassy Veldstra
译文:伯乐在线 - 至秦
链接:http://web.jobbole.com/86556/
在产品应用程序中,内存泄露是很常见的。幸运的是通常不难发现它们。 接下来是一个练习的演练,这个练习是Igor Soarez 和我最近在 WDCNZ 上所教授 Node.js 性能专题课程的一部分。
问题
我们有一个服务运行在一个反向波兰表示法计算器(RPN,Reverse Polish Notation)的产品上,这个产品是基于 WebSockets 实现的。在这个程序的生命周期里,内存使用看上去不断地增长,尤其明显的是当我们使用这个服务时,内存使用会出现一个尖峰。
这个服务的源代码可以在我们 Github 的 calc-server 下找到。你可能仅通过检查这段很短的代码就可以找到这个内存泄露,但我们的想法是不阅读代码就能确定这个泄露。
快速地看一下 client.js 以便知道客户端是如何使用我们的服务。
在继续确认问题和分析程序前,我们先在本地安装和运行服务器。 我们利用 heapdump 来分析 Node 程序里的堆,以便找到一个解决方法。
确认诊断结果
检查内存的使用
在产品中,你可以使用一个应用程序性能管理(APM,Application Performance Management)方案去监控 RAM 使用,如果出现问题它会提醒你。
在这个练习中,我们将结合使用古老的 ps 和 top,以及一些用来引发问题的负载检测方法,以便我们证实问题的存在(检查是代码中的哪些改动造成这样的预期效果)。
使用 ps 来检查进程的内存使用情况:
ps -p $PID -o rss,vsz
(使用 pgrep -lfa node,你可以找到 Node.js 进程的 PID)
这告诉我们进程的驻留集大小和虚拟内存大小。(译者注:RSS,即进程所使用的非交换区的物理内存)
RSS 用来表示这个进程当前正在使用的 RAM 大小。包含所有的栈和堆内存,也会包含共享库的内存,只要那些库的页面实际上是在内存中。
VSZ 表示这个进程上有多少内存可以用。包含交换区的内存和所有的共享库。VSZ 包括 RSS,而且通常比较大。
我们可以使用 top 来观察内存使用的实时情况:
top -pid $PID
OSX 说明:OSX 即使在有大量空闲 RAM 可用时,也会积极地压缩它认为“不活动”进程的内存页面。这可能会导致对于一个空闲的 Node.js 进程, ps 和 top 显示内存占用较小,一旦这个进程重新开始做事情,内存占用就会激增。
(在Node 进程中调用 process.memmoryUsage 函数也可以测量 RAM 使用情况,但是我们不准备使用这种方法。)
测量内存使用的题外话
现代操作系统的内存管理是相当复杂的,对于“我的进程使用了多少内存”这个问题,并没有一个简单的答案。
这里我们要寻找的是确认内存使用在负载时仍然在增长 —— 而不是使用内存的准确数量。
扩展阅读:
http://stackoverflow.com/questions/860878/tracking-actively-used-memory-in-linux-programs/872456
https://mail.gnome.org/archives/gnome-list/1999-September/msg00036.html
http://bmaurer.blogspot.co.uk/2006/03/memory-usage-with-smaps.html
确认增长
下一步就是要在服务器上增加一些负载来确认内存的增加。我们使用 Mingigun,一个简单却强大的负载测试工具(实际上由你自己开发)来做这件事。
我们的负载测试脚本(包含在 test.json 这个repo中)每秒将创建10个新的用户会话,一共持续120秒的时间。每个用户都调用我们的服务去进行两个数字的加法操作:
运行下面脚本,安装 Minigun:
npm install -g minigun
然后运行它:
minigun run load_test_for_calc_server.json
当测试运行的时候,使用 top 监控你的 cal-server。我们应该看到在两分钟里内存使用量稳定地增长。
在我电脑上,内存使用量在 Node 运行后,从 14 MB 增加到 38 MB。重新多次运行这个脚本,内存使用量增加到 110 MB。好吧,休斯顿,我们的确有个问题。
题外话:读者的练习
我们能确认存在一个内存泄露吗?会不会仅仅因为系统还有很多空闲的内存,所以垃圾收集器就不再进行收集?
我们可以通过如下步骤强制运行垃圾收集来加以确认:
使用 –expose-gc 标记运行服务器:node –expose-gc server.js 这让 JS 代码中 gc() 函数可用,可以强制进行收集。
用 process.on 创建一个 SIGUSR2 的处理程序,process.on会调用gc()。
重新运行负载测试,通过下面命令让进程运行垃圾收集 kill -SIGUSR2 $(pgrep -lfa node | grep server.js | awk ‘{print $1}’),看会有什么不一样。
堆分析
heapdump 模块让我们对内存中的对象进行快照。接着我们可以使用 Chrome 开发工具来仔细查看它们以便找到内存泄露的对象类型,这样将帮助我们查明应用程序中有问题的代码。
我们采取如下步骤:
在程序启动后使用 heapdump —— 这作为我们的基准快照
运行一个负载测试程序来引起内存的增加
再一次进行堆快照。这个快照和基准快照的差别就是那些被挂起的对象不能被 GC 回收再利用。
用 heapdump 进行快照
首先,我们用 npm install heapdump 安装 heapdump,server.js 里会使用到它(或者在应用程序的 index.js 里)。
接着我们可以发送 SIGUSR2 给 Node 进程,这样将会把一个 heap 快照写到进程的工作路径下(一个名字类似 heapdump-706203888.138768.heapsnapshot的文件)
在这个练习中,我有三个快照:(1)服务器刚启动的;(2)负载测试过程中的;(3)负载测试结束后的。
说明
某些版本的 Node 或 Io.js,和 Chrome 有一些已知的兼容性问题,开发工具不能正确计算保留的大小和完整显示保留树。我使用的是 Node 0.12.7 和 Chrome 42,如果你偶尔遇到类似问题你可以升级 Node 或者使用 nvm。
另一个可能需要注意的问题是,当进行快照时,你系统中的可用 RAM 需要有 2 个 heap 大小,否则你将看到空的 heap 快照文件或者 内存耗尽的消息(OOM,Out of Memory)。
使用 Chrome 开发工具
一旦我们有了快照,就可以使用开发工具进行分析。
Chrome DevTools memory profile
一旦加载上,我们就可以观察堆。
像保留数量这些不同的术语,可以参考如下内容:
https://developers.google.com/web/tools/profile-performance/memory-problems/memory-101?hl=en
heap snapshot in Chrome DevTools
我们想使用比较视图来进一步定位内存使用量增加的原因。
heap snapshot difference
这个视图告诉我们和刚开始的快照相比,我们有 813 个新的 smalloc 类型对象,它们一共占用了 12.4 MB。
我们对于“保留数量”和“# 新的” 这两列很感兴趣 —— 这个例子中我选择关注 smalloc,因为这些对象和内存增长有关。
object's retaining tree
深入研究这些对象的其中一个,我们可以推出 cleanup() 函数是用来监听 SIGINT 事件的,涉及到一个叫做 clients 的数组,它是保存WebSocket 连接的。SIGINT (和 SIGTERM)是让进程退出的信号,是由进程的管理者发出的,类似 Upstart 或者 init(或者在终端按下 Ctrl+C)。这个例子里,cleanup() 函数在进程退出前断开所有连接的 WebSocket 客户端。我们猜测在进行堆快照时,服务器有 813 个活动的 WebSocket 连接。
负载测试结束后,看看堆是什么样子的:
heap snapshot difference after load-test
糟了,看上去不妙。WebSocket 连接的引用数目已经增加到1649个,这只有在仍有客户端连接到服务器的时候才算合理,但是因为(a)在我们快照前,负载测试已经结束;(b)在快照被写入前,GC 已经执行了,我们知道在代码中(我们可以开始查看了)有些断开连接的引用没有被去除。
修正这个问题就作为一个练习留给读者。:)
备注:如果Node.js 性能是一个深入你内心的主题,你可能会感兴趣知道 Igor Soarez 和 我碰巧正在写一本这方面的书。请前往 nodeperformance.com 登记预览(暂定在这个秋天)。
译者简介
至秦:Linux,Networking
打赏支持作者写出更多好文章,谢谢!
【今日微信公号推荐↓】
更多推荐请看《》
以上是关于如何调试 Node.js的内存泄露的主要内容,如果未能解决你的问题,请参考以下文章