如何定位 Node.js 的内存泄漏

基础知识

Node.js 进程的内存管理,都是有 V8 自动处理的,包括内存分配和释放。那么 V8 什么时候会将内存释放呢?

在 V8 内部,会为程序中的所有变量构建一个图,来表示变量间的关联关系,当变量从根节点无法触达时,就意味着这个变量不会再被使用了,就是可以回收的了。
而这个回收是一个过程性的,从快速 GC 到 最后的 Full GC,是需要一段时间的。
另外,Full GC 是有触发阈值的,所以可能会出现内存长期占用在一个高值,也可以算是一种内存泄漏,可以从《一次 Node.js 应用内存暴涨分析》中找到例子。还有一种就是引用不释放,导致无法进入 GC 环节,并且一直产生新的占用,这一般会发生在 Javascript 层面。

所以,定位内存泄漏问题,一般方案就是找那些不被使用又不会被释放的变量,处理了这些变量,问题一般就可以解决了。如果是 Node.js 底层变量不释放,除了提交 issue 等待解决外,只能通过优化启动参数来解决。

如何找出并解决问题

工具

工欲善其事必先利其器,在排查时,我们还是需要一些工具来帮忙的。

devTool

这个是今年初出的 Node.js 调试工具,基于 Electron 将 Node.js 和 Chromium 的功能融合在了一起。操作起来比 node-inspector 方便,开放的 Timeline 功能还是比较实用的,虽然不是实时显示。
仅需要 devtool xxx.js,还可以通过 .devtoolrc 来进行参数定制,具体见 GitHub

heapdump + chrome devTool

这个是比较传统的定位内存泄漏的组合。heapdump 可以直接在代码中调用生成内存快照,然后将快照文件导入到 chrome devTool 进行分析,之后操作其实和前者就差不多了。不过,这个方案和前者有一点区别就是,前者实际还是在浏览器环境中,所以生成的内存快照会有一些 DOM 对象的存在,会有一定的干扰。而这个方案,是直接调用底层 V8 的方法,生成的快照只有 Node.js 环境中的对象。

memwatch

这个可以在代码里直接使用,实时检测内存动态,当发生内存泄漏的时候,会触发 ‘leak’ 事件,会传递当前的堆状态,配合 heapdump 有奇效。详见 memwatch

流程

一、重现问题

对于垃圾回收,V8 引擎有很复杂的逻辑来决定什么时候进行回收。很多时候,当我们发现 Node.js 进程所使用的内存快速增长的时候,并不能确定是否是内存泄漏导致的,很有可能是程序设计问题,导致内存的不合理利用。只有当垃圾回收触发,未使用内存被释放后,内存增长还在持续,我们才能确定是发生了内存泄漏。

隐藏的内存泄漏问题,大多是有触发条件的,重现问题是需要这些条件的,所以我们在平时写代码的时候,可以将一些重要环节的参数细节打印在 log 中,这样我们在重现问题是就不会摸不着头脑,乱试一气。

有了参数可以用来重现问题,接下来要确定问题。我们要确定,这部分内存是否没有被 GC 正确释放。那么问题来了,我们如何知道程序进行了垃圾回收呢?很显然,等待并不是办法,我们要主动。

在 Node.js 的启动参数中,提供了暴露手动调用 GC 方法的参数,即 --expose-gc。我们用这个参数来启动应用后,就可以在代码中调用 global.gc() 手动触发垃圾回收操作。同时,使用 process.memoryUsage().heapUsed 获取进程运行时所占用的内存。如果 GC 之后,内存依然没有下降,就可以确定是内存泄露了。

二、生成内存快照

既然内存是问题,我们就需要获取程序运行的内存快照来帮助定位问题。但内存快照并不是随便打得,是有一定技巧的。

我们至少要生成三次内存快照,才能更好的定位问题。这三次中又一次要在问题出现前生成,之后可以在问题持续的过程中生成两次或更多。

为什么要这样做呢?理解起来很简单。第一次是为了获取正常情况下的堆栈信息,而在问题出现后,堆栈信息一定会发生变化,有了第一次的信息,我们才好进行后面的比对,过滤一些无用的信息。而后两次的快照,用来比对某一对象的堆栈变化,来确定是否是有问题的对象。下面会详细应用到。

三、定位问题

用 devTool 的可以忽略下面的过程:

打开 Chrome Devtools ,进入到 Profiles 选项卡,点 Load 按钮,加载之前生成的快照。

对于内存快照,有四个视图,Summary,Comparison,Containment,Statistics,这里面常用的是前三个。

在 Summary 视图中,我们可以看到当前快照的全部信息,以及多个快照之间的信息。在列表里显示的都是对象的构造函数名字,可以先忽略被括号包裹的对象,优先观察其他的对象,最后再来看他们。后面的 shallow size 表示的是对象自身的大小,retained size 表示的是对象和它依赖对象的大小,一般是 GC 不可达的。

在 Comparison 视图中,我们可以进行多个快照之间的对比,这个用处比较大,如果我们将前两次快照进行对比,可能比较快速的定位出问题的对象。注意观察 New、Deleted、Delta,如果是内存泄漏的对象,可能是一直在 New,而没有 Deleted。

在 Containment 视图中,我们可以查看整个 GC 路径,当然一般不会用到。因为展开在 Summary 和 Comparison 列举的每一项,都可以看到从 GC roots 到这个对象的路径。通过这些路径,你可以看到这个对象的句柄被什么持有,从而定位问题产生的原因。值的注意的是,其中背景色黄色的,表示这个对象在 Javascript 中还存在引用,所以可能没有被清除。如果是红色的,表示的是这个对象在 Javascript 中不存在引用,但是依然存活在内存中,一般常见于 DOM 对象,它们存放的位置和 Javascript 中对象还是有不同的,在 Node.js 中很少遇见。

更多的操作方法,可以看这个视频 Memory Profiling with Chrome DevTools 和Memory Management Masterclass。还有 Chrome 的文档 Memory Profiling(旧) 和Memory Diagnosis(新)。讲的还是很详细的。(请自备梯子)

四、解决问题

一般在 Javascript 中存在引用而导致内存泄漏的情况,是比较好处理的,只需要在使用后及时的将引用释放掉即可。

但像 《一次 Node.js 应用内存暴涨分析》 所存在的那种内存问题,是属于底层机制的问题,如果等不了 bugfix,就只能先通过一些启动参数来优化内存管理。常用的参数:

  • --max-old-space-size 限制老生区大小,可以控制内存占用的最大值,即使发生泄漏,也不会让内存占用保持很高。可以根据开启进程数以及是否同机部署来优化。
  • --gc_global 这其实是个 V8 的 debug flag,让 GC 永远都是 Full GC,使用上会有一定的性能损耗,根据应用复杂度不同,损耗不同。

当我们找到问题,进行修复后,重复上面的步骤,确认问题已经被解决。有时可能一次并不能解决问题,所以耐心还是很重要的。

实战

可以在这里下载使用到的代码, GitHub,进入 memory-leak 文件夹。
我们来举个例子,应用上面的步骤排查问题,使用 leak-memory 的例子,代码还有另外一个例子,可以自己实践。

这里我们为了方便,我们使用了 devTool。

devTool leak-memory.js

然后在打开的界面中进入内存快照界面,生成第一次快照。当控制台有输出后,间隔的生成两次快照,结果如下。

我们切换视图,对比下三次快照间的区别,可以看到 Foo 这个对象一直在创建而没有被删除。

我们展开 Foo,选择下面的一个实例,查看它的 GC path,可以看到它一直被 neverRelease 持有引用(黄色),所以没有被释放,之后就可以进行问题的处理了。

去掉 // neverRelease.splice(index, 1); 前的注释,然后在重复上面的步骤,你会发现内存的变化已经正常了。

在使用 devTool 时,可以查看运行时的 memory timeline,如果图像呈现阶梯式增长,一般就是存在内存泄漏问题了。正常的应用曲线会类似于锯齿,如图:

总结

  1. 内存泄漏问题的定位,经验很重要,但有了良好工具的辅助,可以节省很多时间。如果懒得自己一步步的操作,可以接入 alinode,这个可以帮助你很方便的生成快照等运行时数据,并有一定的分析辅助,还是方便的。
  2. 你可能看到很多内存分析的文章会有一些图来表示内存的增长,可以使用 python 来快速生成相关的图片,使用 matplotlib.pyplot 这个包。

参考

转载自:http://taobaofed.org/blog/2016/04/15/how-to-find-memory-leak/

作者:凌恒

时间: 2024-10-31 06:22:34

如何定位 Node.js 的内存泄漏的相关文章

Node.js中内存泄漏分析

内存泄漏(Memory Leak)指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况.如果内存泄漏的位置比较关键,那么随着处理的进行可能持有越来越多的无用内存,这些无用的内存变多会引起服务器响应速度变慢,严重的情况下导致内存达到某个极限(可能是进程的上限,如 v8 的上限:也可能是系统可提供的内存上限)会使得应用程序崩溃. 传统的 C/C++ 中存在野指针,对象用完之后未释放等情况导致的内存泄漏.而在使用虚拟机执行的语言中如 Java.JavaScript 由于使用了 GC (Garbag

穆客带你快速定位Node.js内存泄露

在7月7日的云栖TechDay活动上,来自阿里云的穆客给大家分享了<如何快速定位Node.js内存泄露>话题.此次分享主要包括Node.js和APM的简单介绍.Node.js内存管理.Node.js内存泄露及其排查过程四个方面. 下面是现场分享观点整理. 大家好,我是来自阿里云的穆客,今天分享的是关于Node.js方面的故障排查.内存泄露的话题. Node.js和APM 很多人应该都知道Node.js,它是一个运行于服务端的基于Chrome V8引擎的 JavaScript 运行环境,Node

记一次 Node.js 应用内存暴涨分析

起因 之前 TMS 在运行时 CPU 中占用率和内存占用一直很高,导致应用运行状态不是很良好,需要频繁重启.经过排查,找出了部分原因: 使用的 html-minifier 模块有问题,如果输入的内容是一个有错误的 HTML 结构,会使解析进入死循环,导致 CPU 占用率 100%. 在使用 vm 模块时,使用姿势错误,导致内存占用无法释放,使内存占用暴涨. 第一个问题我们今天不予讨论,主要来说一下第二个问题. VM(Virtual Machine) 模块 我们就先了解下 VM 这个模块. 从它的

浅析Node.js中的内存泄漏问题

  这篇文章是由Mozilla的Identity团队带来的 A Node.JS Holiday Season系列文章的首篇,该团队上个月发布了 Persona的第一个测试版本.在开发Persona时我们构建了一系列的工具,包括了从调试,到本地化,到依赖管理以及更多的方面.在这一系列的文章中我们将与社区分享我们的经验和这些工具,这对任何想用node.js建立一个高可用性服务的人都很有用.我们希望您能喜欢这些文章,并期待看到您的想法和贡献. 我们将从一篇关于Node.js的实质性问题:内存泄漏的主题

浅析Node.js中的内存泄漏问题_node.js

 这篇文章是由Mozilla的Identity团队带来的 A Node.JS Holiday Season系列文章的首篇,该团队上个月发布了 Persona的第一个测试版本.在开发Persona时我们构建了一系列的工具,包括了从调试,到本地化,到依赖管理以及更多的方面.在这一系列的文章中我们将与社区分享我们的经验和这些工具,这对任何想用node.js建立一个高可用性服务的人都很有用.我们希望您能喜欢这些文章,并期待看到您的想法和贡献. 我们将从一篇关于Node.js的实质性问题:内存泄漏的主题文

nodeJs内存泄漏问题详解_node.js

之前一次偶然机会发现,react 在server渲染时,当NODE_ENV != production时,会导致内存泄漏.具体issues: https://github.com/facebook/react/issues/7406 .随着node,react同构等技术地广泛运用,node端内存泄漏等问题应该引起我们的重视.为什么node容易出现内存泄漏以及出现之后应该如何排查,下面通过一个简单的介绍以及例子来说明. 首先,node是基于v8引擎基础上,其内存管理方式与v8一致.下面简单介绍v8

Node.js巧妙实现Web应用代码热更新_node.js

背景 相信使用 Node.js 开发过 Web 应用的同学一定苦恼过新修改的代码必须要重启 Node.js 进程后才能更新的问题.习惯使用 PHP 开发的同学更会非常的不适用,大呼果然还是我大PHP才是世界上最好的编程语言.手动重启进程不仅仅是非常恼人的重复劳动,当应用规模稍大以后,启动时间也逐渐开始不容忽视. 当然作为程序猿,无论使用哪种语言,都不会让这样的事情折磨自己.解决这类问题最直接和普适的手段就是监听文件修改并重启进程.这个方法也已经有很多成熟的解决方案提供了,比如已经被弃坑的 nod

记一次 Node.js 内存泄漏排查

首先感谢 alinode 团队 https://alinode.aliyun.com 的支持. 大概一个月前,在 alinode 管理页面看到内存占用成锯齿状上升,虽然上涨速度很慢,但是最低点与最高点都在稳定上涨,意识到应该是内存泄漏了. 虽然有关内存泄漏方面的文章读了一些,也知道需要看内存快照,内存时间线等日志来分析内存泄漏,但是真正自己上手时还是有些懵逼了.使用 alinode 将程序运行不同时间点的内存快照导出然后对比分析(这时使用的是 devtools),因为对于 devtools 了解

Android应用内存泄漏的定位、分析与解决策略

Hello,大家好,我是Clock.翻了一下简书,发现有一个多月没有更新博客,本来今天打算和妹纸去电影院看<你的名字>,然后再去到处浪的. 结果因为妹纸公司临时有事,她不得不回公司一趟... 然后我也只能宅家里了,既然妹纸不在家,刚好最近一直在为项目做内存泄漏的优化工作,那就来写一点个人总结好了. 什么是内存泄漏 对于不同的语言平台来说,进行标记回收内存的算法是不一样的,像 Android(Java)则采用 GC-Root 的标记回收算法.下面这张图就展示了 Android 内存的回收管理策略