如何在 Node.js 中发现 JavaScript 内存漏洞
Posted OSC开源社区
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何在 Node.js 中发现 JavaScript 内存漏洞相关的知识,希望对你有一定的参考价值。
几个月前,我还在调试 Node.js 的内存漏洞。我发现有大量谈论这个问题的文章,但是经过一番仔细阅读,我依然很困惑,究竟我该做些什么才能调试这些漏洞。
我写这篇文章的目的是为了给大家提供一个小指南——如何发现 Node 中的内存漏洞。我将在下面列出这个简单易操作的方法,(在我看来)它属于调试 Node 内存漏洞的首步步骤。在某些情况下,这个办法或许不算详尽,所以我会链接出你可能考虑到的其他一些资源。
javascript 是拥有垃圾回收机制的语言,因此, 所有内存的使用以节点化的方式处理并被V8 JavaScript 引擎自动分配,释放。
V8如何释放内存呢?V8以图的方式保存程序中的所有变量。JavaScript中有四个数据类型:Boolean, String, Number, 和Object,前3个是基本类型,并且它们只持有分配给它们的数据(例如,一个字符串)。关于Objects,JavaScript 中一切皆对象(例如:数组是对象),Objects类型能够保留其他对象的引用。
V8将间歇性的遍历内存图,尝试识别那组内存数据节点不再与根节点连通.如果一个节点与根节点不再连通,V8假设该数据不再被使用并且释放该内存.这个过程就是垃圾回收
JavaScript的内存泄漏发生在一些不再需要使用的数据依旧能够与根节点连通时。V8假设数据依旧被使用并且不会释放所占用的内存。注意垃圾回收机制不是一直运行 ,通常V8能够在合适的时候触发垃圾回收,举例说明, V8间歇性的运行垃圾回收,或者在剩余空闲内存少时,它会触发一个阻塞其他活动的垃圾回收。 每一个过程能使用的内存节点数量是有限的,所以V8必须以良好的机制运行。
想象下你有一个app存在很多内存泄漏,伴随内存节点的使用内存将要被耗尽,这时V8触发了阻塞其他活动的垃圾回收。但是很多内存节点依旧能够与根节点连通,只有非常少的内存节点被释放,内存占用还是很高。
不久后,伴随内存节点的使用内存又将要被耗尽,触发另一个垃圾回收.就如你所知道的一样,你的app将进入垃圾回收的死循环,只是试图保持过程运行。因为V8花费大量的时间进行垃圾回收,只会非常少的资源去运行该运行的程序。
正如我前面所指出的,JavaScript的V8引擎使用了一套复杂的逻辑来觉id那个什么时候垃圾收集应该运行。明白了这个,就会知道尽管我们可以看到用于一个Node现成的内存在持续地增涨,我们还是不能确定自己是否目击了一次内存泄露, 直到我们知晓了垃圾收集已经运行起来,让不再被使用的内存可以被清理出来。
值得庆幸的是,Node允许我们手动触发垃圾回收,而这是在尝试确认一个内存泄露问题时我们应该要做的第一件事情。这件事情可以借助在运行 Node 时带上 --expose-gc 标识(例如 node --expose-gc index.js)来完成。一旦node以那个模式运行,你就可以用编程的方式通过从你的程序调用 global.gc() 来在任何时刻触发一次垃圾回收。
你也可以借助于调用 process.memoryUsage().heapUsed 来检测进程使用的内存数量。
通过手动触发垃圾回收并检测堆的使用情况,你就能够判别出自己是否实际地观察到了程序中的一次内存泄露。
我已经创建了一个简单的内存泄露程序,你可以看看这儿:https://github.com/akras14/memory-leak-example
你可以把它clone下来,然后运行 node --expose-gc index.js 将它跑起来。
1、每过5毫秒生成一个随机的对象并将其存储到两个数组中,一个叫做 leakyData 而另外一个叫做 nonLeakyData。每过5毫秒我们将清理掉 nonLeakyData 数组, 而我们将会“忘记”清理 leakyData 数组。
2、每过两秒程序就会输出内存的使用数量 (并发生堆的转储,而我们会在下一节对此进行更详细的描述)。
如果你使用 node --expose-gc index.js (或者是 npm start)来运行这个程序, 它就会开始输出内存的统计信息。我们让它跑一两分钟然后使用 Ctr + c 快捷键杀掉它(进程)。
你会看到内存使用在快速地上涨,尽管每两分钟我们都是触发了垃圾回收的,就在我们获得这份统计数据之前:
如果你以图表展现数据,那么内存的增长态势会表现得更加明显。
注意:如果你比较好奇我是如何做到以图表展现数据得,请继续都下去。如果不感兴趣的话请跳到 下一节。
我是将输出的统计数据保存到了一个 JSON 文件中,然后用几行Python代码读入它并以图表展示出来。为避免混乱,我已经将其保存造一个独立的分支中,而你可以在这儿check出来: https://github.com/akras14/memory-leak-example/tree/plot
相关的部分内容如下:
你可以check出 plot 分支,然后跟往常一样运行程序。一旦你运行完 plot.py ,就会有图表生成出来。你会需要在机器上安装好 Matplotlib 库,才能让程序跑起来。
好了,我们已经重现了问题,接下来该如何呢? 现在我们需要搞清楚问题出在哪儿,然后解决它。
我是使用了一个 node-heapdump 模块,你可以在这儿找到:https://github.com/bnoordhuis/node-heapdump
为了能使用 node-heapdump, 你只需要这样做:
3、在类Unix的平台上调用 kill -USR2 {{pid}}
如果你从前没有遇到过 kill 这部分的话,其实它是 Unix 中的一个命令,你可以用它来(在其它东西中) 发送自定义信号 (也就是用户信号(User Signal))给任何正在运行的进程。Node-heapdump 被配置为当它收到一个用户信号二,也就是 -USR2, 后面带上进程id,就要做一次进程的堆转储。
在我的示例程序中,通过运行 process.kill(process.pid, 'SIGUSR2'); ,我对 kill -USR2 {{pid}} 命令进行了自动化,这里 process.kill 是针对 kill 命令的一个封装,SIGUSR2 是 -USR2 的Node表示方式, 而 process.pid 会获取到当前 Node 进程的 id。我会在每次垃圾回收之后运行这个命令来获得一个干净的堆转储。
我想 process.kill(process.pid, 'SIGUSR2'); 是不会在 Windows 上面运行的, 不过你还是可以运行 heapdump.writeSnapshot() 来实现同样的事情。
如果第一时间就使用 heapdump.writeSnapshot() 的话,这个示例也许会稍微简单一点,不过我想提一提的就是,你还是可以在类 Unix 平台上使用 kill -USR2 {{pid}} 信号来触发一次堆转储,而这可能会拍上用场。
下一节会讲到我们如何使用生成的堆转储来堆内存泄露进行隔离。
在第二步中,我们做了堆转储,但是我们将至少需要3块,你不久就会明白为什么要这样。
你一旦有了堆转储。马上去谷歌浏览器,打开浏览器开发者工具(windows系统快捷键是F12,Mac上是Commands+Options+i)。
一旦在开发者工具里导航到“Profiles”的标签,在屏幕的底部选择“加载”按键,导航到你导入的第一块对转储并且选中它。对转储将会加载进浏览器里,如下图所示:
继续把另外2块堆转储加载到view视图中。例如,你可以使用你导入的最后2块堆转储。最重要的事情是,堆转储必须依照顺序来加载。你的文件夹导航大概如下图所示:
你可以从图中获取的信息是,堆继续随着时间的推移而增长。
堆转储一旦加载好,你将会在文件夹导航栏看见许多子视图,并且它们很容易丢失。但是,我发现有一个视图特别有用。
点击你导入的最后一个堆转储,它将会马上呈现“概要”视图给你。到左边的概要导航栏下拉,你可以看见另一个全部的下拉菜单。点击它并且选择“对象被分配在你第一块堆转储与第二和最后一块堆转储之间”,如下图所示:
它将会展示我们在第一块与最后一块堆转储所分配的所有对象。事实是,这些对象依然存在于你的堆中,引起关注和值得研究,由于它们已经被垃圾回收器回收。
事实很令人吃惊,但是并不是靠着直觉来查找,并且容易被忽略。
注意到阴影大小代表对象本身,而剩下的数量代表对象与所有子对象。
似乎还有5个条目保存在我的快照里(数组)、(编译的代码)、(字符串)、(系统)以及简单类。
它们看起来只有简单类似曾相识,由于它来自如下示例程序中的代码。
它可能是诱人的开始,通过(数组)或者(字符串)。在概要视图中所有对象由它们的的构造函数名称来分组。对于数组或者字符串来说,那些构造函数内嵌到JavaScript引擎里。当你的程序非常确定:通过构造函数创建来传递一些数据,你也会在那里获取一些脏数据,使得我们更难找出内存泄露的根源。
这就是为什么我们一开始跳过那些步骤,看看这样你是否能发现可疑的内存,比如在示例程序的简单类构造函数里。
在简单类的构造函数点击下拉菜单,从结果列表中选择任意被创建的对象,将会在窗口下部填充剩下的路径(请看上图)。从那里很容跟踪我们数据中的内存泄露部分。
如果在你的app中你不够幸运,像我在app里遇到问题一样,你应该查找内部构造方法(比如字符串),从那里尝试找出内存泄露的来源。在这种情况下,关键是要尝试其他组别的值,经常出现在一些内部构造函数里,尝试使用提示指向一个内存泄漏的可疑之处。
比如,在示例程序中,你可以观察到许多字符串,看起来像是随机数转换成字符串的。假如你检测他们的原始路径,Chrome浏览器开发者工具将会指出内存泄露的数组。
确认并解决了一个可疑的内存泄漏之后,你就应该在你的堆使用中看到很大的不同。
然后像第一步描述的那样重新运行应用,你将会观察到下面这样的输出:
如果我们绘制数据图表,就会得到类似于这张图:
注意在内存使用初始时的尖峰时刻仍然存在,当程序等到稳定时就会正常。注意那个尖峰时刻,在你分析的时候不要把它当做内存泄漏。
1、在尝试重现并验证一个内存泄漏问题时手动触发垃圾回收。你可以在运行 Node 时带上 --expose-gc 标记,并且在你的程序里面嗲用 global.gc() 。
2、做至少3次堆的转储(Heap Dump),要使用 https://github.com/bnoordhuis/node-heapdump
以上是关于如何在 Node.js 中发现 JavaScript 内存漏洞的主要内容,如果未能解决你的问题,请参考以下文章
今日分享Node 核心和 Node eventLoop
node.js 概述与安装以及环境搭建
10+ 最佳的 Node.js 教程和实例
10+ 最佳的 Node.js 教程和实例!
VUE1安装node.js
为什么要用 Node.js