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

大家好,我是来自阿里云的穆客,今天分享的是关于Node.js方面的故障排查、内存泄露的话题 。
Node.js和APM
很多人应该都知道Node.js,它是一个运行于服务端的基于 V8引擎的运行环境,Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效 。Node.js特别适合于web应用或API服务器,例如手机上的APP,如果它的服务器端采用Node.js来搭建,整体的开发效率会提高很多倍,而且也非常适合IoT服务端应用 。
图一 Node.js和APM
同样的任务 , 用C或C++开发可能需要一个月的时间;采用Node.js开发的话,一两天就可以搭建出原型 。因此,对于一些初创型、中小型企业在需要业务快速上线的时候,或者公司里需要快速做原型验证,如果系统不是特别复杂的时候,Node.js是非常适合的 。即使业务量上来后,Node.js也是非常容易扩展的 , 现在像Uber/沃尔玛/阿里等规模的企业里面,Node.js也获得了很好的应用就能说明一些问题 。
应用性能管理(APM),是对企业系统即时监控以实现对应用程序性能管理和故障管理的系统化的解决方案 。APM的覆盖范围包括五个层次的实现:终端用户体验,应用架构映射,应用事务分析,深度应用诊断,和数据分析 。总结成一句话概括:用一切手段把应用的整个链路监测起来,将中间的慢路径或热点找出来,进行优化后提高用户的体验 。
现在,Node.js使用已经非常普遍了 。在上搜索的热度 , Node.js处于领先地位 。同时 , Node.js相比于其他语言的发展速度是最快的,据2016年1月份Node.js社区调查显示:Node.js目前在全球大约有350万用户,年增长率为100% 。
Node.js堆构成
Node.js的内存管理是不需要用户关心的,js代码也看不到内存管理的一些细节的,使用者无法像c语言里那样深入分配、管理内存,只能去创建一些对象,当这些对象不再使用时 , v8引擎后期进行垃圾回收,进行内存管理 。
图二 Node.js堆组成
这些对象是分配在堆上面的,对于用户代码而言,Node.js堆可以分为以下几个区域:
图三 内存使用情况
平台可以将Node.js代码运行时的状态显示出来 。上图显示的是内存使用情况,可以显示整个RSS、堆中的数据以及堆里面的空间使用大小 。其中RSS包括堆内堆外所有的; 表示的是V8从操作系统分配出来的空间;表示已经在用空间 。Rss和中间这一块是堆外内存,对外内存从用户的角度考虑主要包括和C++等模块分配出的内存 。
GC信息是指运行时GC时间占整个程序运行时间的占比 , 由于进行GC时,用户代码是停掉的,所以这个数据是越小越好 。该平台可以将堆上空间内每个Space占用大小显示出来 , 上图显示的数据是实时数据(来自CNPM),可以看到堆空间主要组成是 。
垃圾回收机制
下面简要介绍下垃圾回收(GC)过程 。在前面有提到,当堆分为几代之后,重新分配的对象大部分是放到之中(如果这个对象非常大 , 超过1M , 则会放到大对象区内) 。新生带分为两类:New Space(from)和New Space(to) 。对象在最初生成时,存放在New Space(from)内;当New Space(from)占满之后,进行一次GC操作 。
图四 Short GC机制
在V8内有一个根对象(最简单的例子就是全局变量),GC时从根对象内进行遍历:如果另外一个对象被根对象引用了,则表示它是一个活的对象;接着,逐层进行查找 , 如果发现一个对象已经没有被任何一个对象指向,则认为该对象已死亡;需要将活对象先标记 , 再把它复制到New Space(to)中,当全部复制完成后;将Space(from)和New Space(to)的指针互换,全部复制后只保留活对象,那么死对象的空间就被空出来了;当有新对象生成时,就可以在这里面生存 。
如果经过两次GC操作之后,一个对象仍然存活 , 则将其移动到老生带内 。老生带空间很大,默认为1.4G,堆内几乎所有数据都存在老生带中 。
图五 Full GC机制
是专门针对新生代的内存处理的,另外一种针对老生代的内存处理策略是标记清理(Mark-Sweep-) 。标记清理开始时需要遍历整个老生代,将活的对象标记出来(在图中用绿色颜色标记活对象);将死的对象放到一个列表内 , 如果两个相邻的对象都已经死掉,则二者连在一起形成列表,最后形成mark sweep 。简单地说,就是将死亡的对象标记后放到free list内,当有新对象从新生代移过来时,只要空间足够就可以继续使用 。
这里内存是按页进行管理的,每一页大小为1M 。如果发现mark sweep多次复制之后,内存呈现碎片化;则需要进行mark,将活的对象靠在一起 , 释放出大量空间供新对象使用 。
由于老生代自身很大,每次进行全量操作时,会导致代码停掉1秒甚至更多时间,这个是无法接受的 。因此,V8对其进行了优化,采用增量式标记 , 保证用户代码停顿不会很久 。
内存泄露
图六 内存泄露的因素
V8已经帮助用户实现了内存管理,用户无需再关心内存的释放和分配,那么为什么还存在内存泄露呢?
除了V8本身的Bug之外,主要的原因是变量释放问题 。如果有变量忘记释放 , V8则认为总是存在从根对象到该对象的路径,因此那部分内存不会被释放 。堆上泄露主要包括全局变量、闭包和定时器等 。前两者好理解 , 定时器主要分为两种:一种是常用的接口,超时之后就释放了;另一种是(间隔性定时器),设置时如果没有用变量把它记下来 , 则就会永远存在那里 , 不被clear掉,进而导致各类问题发生 。
堆外的内存泄露主要发生在和C++扩展模块 。如果是C++模块内存泄露了 , 则比较麻烦 , 目前尚不存在gdb外好用的排查手段 。
如何解决内存泄露问题?
图七 如何解决问题?
碰到内存泄露的问题该怎么办呢?
目前的整体思路主要有两种:一种是重启,将整个服务进行重启 , 这种方式虽然暴力,但是可以解决问题 。当业务量较少或者代码与状态无关时 , 重启带来的影响不大;但如果业务比较复杂的话,重启过程较长则对用户的影响就非常大了 。
另一种方法是一个优秀程序员应该采用的方法,找到内存泄露的那个Bug , 将其解决掉 。
内存泄露定位方法
对堆上的内存泄漏的定位,目前存在这几种方法:、v8+ node (Debug模式)、node-和 。前三种都是社区内常用的方法 , 最后一种使阿里云自行开发的工具 。

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

文章插图
图八 方法
是在Node.js刚推出不久之后 , 由个人所开发的 。它的基本思路是通过添加C++模块提取V8中的数据;之后通过TXT进行显示的提取的数据信息 。
使用需要安装一个NPM模块,在用户代码要引用该模块 , 此外,还要加一些事件,然后可以将所需信息打印出来 。
如果需要判断是否存在内存泄露,需要做两次,第一次记录每个对象的数量、大?。还欢问奔湓僮龅诙危?生成报告里面的会记录这段时间内对象的变化情况 。
v8+ node
图九 v8+ node 方法
v8- + node-,它是一种调试模式 。首先需要用安装v8-(实时做Heap dump的工具),还需要安装node-(图形化展示和操作界面);然后采用Debug模式启动应用;之后再进入node-查看内存泄露情况 。
node-
图十 node-方法
node-使用前需要通过NPM安装一个的模块,在应用内部同样需要引用该模块 。在外部通过发送一个USR2的信号给PID,node-是通过外部来触发的 。如果不知道什么时间出现问题,可以通过在内部添加逻辑来判断堆的大小 。当认为出现问题时 , 可以发信号给自己或调用接口,把堆全部dump出来 。
图十一 方法
【穆客带你快速定位Node.js内存泄露】最后一种方法,首先需要安装的运行时;同时支持在生产环境内部署,此外无需修改用户代码;发现问题时,直接进行;最后生成详细的分析报告 。
相比于前三种方式,可以较为方便、快递地查找到泄漏点,便于后续的修改 。
cpu- 以及其它
图十二 CPU-以及其他
除了支持在线的之外,还支持在线的等操作 。用户程序写好之后,除非用户逻辑写错,否则最后的问题都可以归结为CPU或内存的瓶颈 。
在进程中,如果感觉CPU利用率很高 , 可以创建一个CPU ,在线进行三分钟的CPU,三分钟过后将其关闭 。Node.js如果不用这种方法 , 还有两种方法,但都需要在node启动时就设置参数:一个是Debug参数 , 加个node-可以实现该功能;另一种方法是prof参数 , 可以生成一个prof的文件显示CPU的使用情况 。