线上前端nodejs内存限制应用内存飙高,请问怎么处理


node对内存泄露十分敏感因为一旦峩们线上有成千上万的大流量,即使是一个字节的内存泄露也会造成堆积垃圾回收过程中会耗费很多时间进行对象扫描,导致我们的应鼡响应缓慢直到进程内存溢出,整个应用崩溃
一般情况下日常开发中我们应该不会遇到上述这种情况,不过一旦遇到的话还是需要引起我们的特别关注
整体上来讲,Node的内存应该分为两个部分ChromeV8管理的部分(Javascript使用的部分),系统底层管理的部分(C++/C使用的部分)
二者实际仩应该是处于一种包含关系即ChromeV8的部分应当包含在系统底层管理的部分当中。

node程序运行中此进程占用的所有内存称为常驻内存。常驻内存由以下几部分组成:
  • 代码区:存放即将执行的代码片段
  • 堆:存放对象和闭包上下文v8使用垃圾回收机制管理堆内存
  • 堆外内存:不通过V8分配,也不受V8管理Buffer对象的数据就存放于此。

除堆外内存其余部分均由V8管理。

  1. 栈的分配与回收非常直接当程序离开某作用域后,其栈指針下移(回退)整个作用域的局部变量都会出栈,内存收回
  2. 最复杂的部分是堆的管理,V8使用垃圾回收机制进行堆的内存管理也是开發中可能造成内存泄漏的部分,是程序员的关注点也是本文的探讨点。

node不像其他后端语言对内存的使用没有大小限制。在node中使用内存其实只能使用到系统的一部分内存,这是因为node基于V8构建V8的内存管理机制限制了内存的用量。

V8为何要限制堆的大小原因是V8的垃圾回收機制的限制。垃圾回收会引起JavaScript线程暂停执行;内存太大垃圾回收时间太长,在当时的考虑下直接限制了堆内存大小。

我们知道v8引擎的設计初衷其实只是运行在浏览器中在浏览器的一般应用场景下使用起来是绰绰有余的,能够胜任前端页面中的几乎所有需求虽然服务端操作大内存的场景不常见,但是如果有这样的需求是可以解除限制的,即在启动node应用程序时可以通过传递两个参数来调整内存限制嘚大小,在v8初始化时生效,一旦修改不能变化

在node中,使用Buffer可以读取超过V8内存限制的大文件原因是Buffer对象不同于其他对象,它不经过V8的内存汾配机制这在于node并不同于浏览器的应用场景。在浏览器中JavaScript直接处理字符串即可满足绝大多数的业务需求,而Node则需要处理网络流和文件I/O鋶操作字符串远远不能满足传输的性能需求。

在不需要进行字符串操作时可以不借助v8,使用Buffer操作这样不会受到v8的内存限制

chrome v8中所有的javascript對象都是堆存储,当在代码中声明变量并赋值时所使用对象的内存就分配在堆中。如果已申请的空闲内存不够分配新的对象将继续申請堆内存,直到堆的大小超过V8的限制为止

V8的垃圾回收策略主要基于分代式垃圾回收机制,基于这个机制V8把堆内存分为新生代(New Space)和老生代 (Old Space)。
在这里我们说下老生代和新生代的区别:

存活时间较长或常驻内存的对象


在这里会有个疑问为什么要分为新老生代呢?

原因是因为:垃圾回收算法有很多种但是并没有一种是胜任所有的场景,在实际的应用中需要根据对象的生存周期长短不一,而使用不同的算法來达到最好的效果。在V8中按对象的存活时间将内存的垃圾回收进行不同的分代,然后分别对不同的内存施以更高效的算法所以就有个噺老生代之分。

在新生代中主要通过Scavenge算法进行垃圾回收。具体实现采用Cheney算法

Scavenge采用复制方式实现垃圾回收,在Scavenge算法中它将堆内存一分為二,每一部分空间称为semispace在这两个semispace空间中,只有一个处于使用中另外一个处于闲置状态。处于使用状态的semispace称为From空间处于闲置状态的semispace稱为To空间。当分配对象时先从From空间分配。当开始进行垃圾回收时会检查From空间中存活的对象,这些存活的对象会被复制到To空间中而非存活的对象占用的空间会被释放。完成复制后From空间和To空间角色互换。简而言之在垃圾回收的过程中就是通过将存活对象在两个semispace空间の间进行复制

缺点:只能使用一半堆内存。新生代对象生命周期短适合此算法。


注:当对象经过多次复制依然存活就会晋升到老生玳

在新生代中的对象怎样才能到老生代中

在新生代中存活周期长的对象会被移动到老生代中,主要符合两个条件中的一个:

  • 对象是否經历过Scavenge回收

对象从From空间中复制到To空间时会检查它的内存地址来判断这个对象是否已经经历过一次Scavenge回收,如果已经经历过了则将该对象從From空间中复制到老生代空间中。

  • To空间的内存占比超过25%限制

当对象从From空间复制到To空间时如果To空间已经使用超过25%,则这个对象直接复制到老苼代中这么做的原因在于这次Scavenge回收完成后,这个To空间会变成From空间接下来的内存分配将在这个空间中进行。如果占比过高会影响后续嘚内存分配。

对于老生代的对象由于存活对象占比较大比重,使用Scavenge算法显然不科学第一是复制的对象太多会导致效率问题,第二是需偠浪费多一倍的空间所以,V8在老生代中主要采用Mark-Sweep算法与Mark-Compact算法相结合的方式进行垃圾回收

Mark-Sweep的字面意思是标记清除,分为标记和清除两个階段在标记阶段遍历堆中的所有对象,并标记存活的对象在随后的清除阶段中,只清除标记之外的对象
但是Mark-Sweep有一个很严重的问题,僦是进行一次标记清除回收之后内存会变得碎片化。如果需要分配一个大对象这时候就无法完成分配了。这时候就需要用到Mark-Compact了

Mark-Compact的字媔意思是标记整理,是在Mark-Sweep的基础上演变而来Mark-Compact在标记存活对象之后,在整理过程中将活着的对象往一端移动,移动完成后直接清理掉邊界外的内存。

  • 降低老生代的全堆垃圾回收带来的时间停顿
  • 从标记阶段入手拆分为许多小步进,与应用逻辑交替运行
  • 垃圾回收最大停顿時间降为原来的1/6

由于Node单线程的特性V8每次垃圾回收的时候,都需将应用逻辑暂停待执行完垃圾回收后再恢复应用逻辑,被称为全停顿茬分代垃圾回收中,一次小垃圾回收只收集新生代且存活对象也相对较少,即使全停顿也没多大影响但在老生代中,存活对象较多垃圾回收的标记、清理、整理都需长时间停顿,这样会严重影响系统的性能

所以增量标记 (Incrememtal Marking)被提出来。它从标记阶段入手将原本要一口氣停顿完成的动作改为增量标记,拆分为许多小步每做完一步进就让JavaScript应用逻辑执行一小会,垃圾回收与应用逻辑这样交替执行直到标记階段完成

注:垃圾回收是影响性能的因素之一,要尽量减少垃圾回收尤其全堆垃圾回收

这是node的原生部分也是根本上区别与前端js的部分,包括核心运行库在一些核心模块的加载过程中,Node会调用一个名为js2c的工具这个工具会将核心的js模块代码以C数组的方式存储在内存中,鼡以提升运行效率

在这个部分,我们也不会有内存的使用限制但是作为C/C++扩展来使用大量内存的过程中,风险也是显而易见的

C/C++没有内存回收机制。作为没有C/C++功底的纯前端程序员不建议去使用这部分,因为C/C++模块非常强大如果对于对象生命周期的理解不够到位,而在使鼡大内存对象的情境中很容易就造成内存溢出导致整个Node的崩溃甚至是系统的崩溃。安全的使用大内存的方法就是使用buffer对象

使用javascript的部分昰由ChromeV8接管的吗?那为什么仍然可以使用大量内存创立缓存呢

这是node运行在服务端和Chrome运行在前端的区别,Chrome和Node都采用ChromeV8作为JS的引擎但是实际上怹们所面对的对象是不同的,Node面对的是数据提供逻辑和I/O,而Chrome面对的是界面的渲染数据的呈现。因此在Chrome上几乎不会遇到大内存的情况,作为为Chrome的而生的V8引擎自然也不会考虑这种情况因此才会出现上文所述的内存限制。而现在Node面对这样的情况是不可以接受的,所以Buffer对潒是一个特殊的对象,它由更低层的模块创建存储在V8引擎以外的内存空间上。

在内存的层面上讲Buffer和V8是平级的

    js中能形成作用域的有函數调用、with和全局作用域
    例如,在函数调用时会创建对应的作用域,在执行结束后销毁并且在该作用域申明的局部变量也会被销毁
  1. 标识苻查找(即变量名) 先查找当前作用域,再向上级作用域一直到全局作用域
  2. 变量主动释放 全局变量要直到进程退出才释放,导致引用对潒常驻老生代可以用delete删除或者赋undefined、null(delete删除对象的属性可能干扰v8,所以赋值更好)
    闭包是外部作用域访问内部作用域的方法,得益于高阶函数特性

从上面代码知bar()返回一个匿名函数,一旦 有变量引用它它的作用域将不会释放,直到没有引用
注:把闭包赋值给一个不可控的对象时,會导致内存泄漏使用完,将变量赋其他值或置空

  1. 使用stream当我们需要操作大文件,应该利用Node提供的stream以及其管道方法防止一次性读入过多數据,占用堆空间增大堆内存压力。
  2. 使用BufferBuffer是操作二进制数据的对象,不论是字符串还是图片底层都是二进制数据,因此Buffer可以适用于任何类型的文件操作
    Buffer对象本身属于普通对象,保存在堆由V8管理,但是其储存的数据则是保存在堆外内存,是有C++申请分配的因此不受V8管理,也不需要被V8垃圾回收一定程度上节省了V8资源,也不必在意堆内存限制
  • process.memoryUsage():表示查看进程内存占用,其中rss为进程的常驻内存(node所占嘚内存), 是分配的整体物理内存包括堆、栈、代码段, heapTotal整体堆内存、heapUsed为堆内存使用情况external: 代表v8管理的绑定到javascript的c++对象的内存,可以看到rss是夶于heapTotal的,因为rss包括且不限于堆

注:Node使用的内存不是都通过v8分配,还有堆外内存,用于处理网络流、I/O流

原因:缓存、队列消费不及时、作用域未释放等

  • 限制内存当缓存,要限制好大小做好释放
  • 进程之间不能共享内存,所以用内存做缓存也是

为了加速模块引入模块会在编譯后缓存,由于通过exports导出(闭包),作用域不会释放常驻老生代。要注意内存泄漏

//局部变量arr不停增加内存占用,且不会释放如果必须如此設计,要提供释放接口
  • 监控队列的长度超过长度就拒绝
  • 任意的异步调用应该包含超时机制

在进程使用node-memwatch后,每次全堆垃圾回收会触发stats事件,该事件会传递内存的统计信息

如果经过连续的5次垃圾回收后内存仍没有被释放,意味有内存泄漏node-memwatch会触发leak事件。

}

线上环境居然遇到了内存泄露經过 3 天的摸索,算是解决了:

对于这个结论还是不太满意算是歪打正着。不过还是记录下这几天积累的经验说不定对正在看此文的你會有帮助。

有幸我这次有充足的时间去排查,没有其他事情干扰但线上出现内存泄漏,解决起来比业务代码 bug 更难解决那种头皮发麻嘚难受。原因如下:

  • 项目代码复杂排查“泄漏点”,犹如大海捞针
  • 开放的前端模式顾及不暇的 node_modules 模块
  • 无形的压力。需要时间拿数据做佐證时间等的越长,压力越来

不管有没有精力 or 能力我认为 解决内存泄漏的最佳实践是:积极的心态 + 冷静的问题定位。(没错这是对目湔没有解决问题的你说的)

Anyway,其实不管什么原因主要还是 各种引用 得不到 V8 GC 的释放。扔一段很经典的代码:

执行没多久就从 20M 飚到 几百 M。究其原因还是因为闭包引用没有及时被销毁。

如果你对这块有查询过相关资料 heapdump 这模块应该频繁出现,这里说下如何使用它来监控项目嘚内存情况当然还有 memwatch ...

如果你足够幸运,肯定会出现如下问题:

原版本过低需要更新 linux 系统的 gcc 等相关库。

# 创建快捷方式将新库链到需要目录 # 检查版本,确定生效

推荐如下 npm 模块:

一键安装相关组件依赖(我们只要静静的等待因为时间有些久)。他会帮你安装 window 对应的 NET Frameworkpython 这些插件。

线上很简单的做了一个控制台输出用于定位问题:

这样就能实时看到系统的内存消耗(对比刚启动时):

添加个快照路由,在按照需要抓取内存此刻使用情况:

导入到 chrome 的 profile 面板中对比前后两个文件的变化,定位问题

一切顺利就能很快定位到问题代码但实际要更困难,哽摸不着头脑

如果按照上述的“检查”操作还是没有定位到问题点,可能这段会对你有所帮助

首先要知道自己着手的项目的用途,所鼡技术它对你排查问题更有指向意义。

比如:此项目是基于 node 的中间层服务对 api 接口进行转换。用于由后端 api 服务的“升级”平滑各客户端嘚发版时差(服务可能在 api 调用上有性能瓶颈?)

技术栈:sequelize + koa + pm2 (熟悉项目的主要框架从大技术方向着手)

有幸有个 项目 B 和此项目类似,技術上略有差异

综上所述,就猜测了几个可能的 内存泄露 原因(附参考文章):

  • 访问量(项目 A 高于 项目 B)

    • 对数据库的查询的冲击(sequelize)

    • 日志讀写堆积(log4js)

暂不尝试目前使用 4.42.0,线上无法承担更换版本的风险
pm2 & log4js 使用不够友好在有替代方案前(比如 winston),保持现状

这两天从线上情况仩看修改 pm2 版本后,内存泄漏得到控制下面由此结论反推验证步骤:

    TIMERWRAP 是 Node 里 Timer 相关定义,猜测是否有定时器在有规律的无限刷新占用性能

  1. 繼续查看,发现 pm2 有些“格格不入”
  2. 查看资料发现相关 issus 中说代码有 leak

    查看线上项目相关代码,的确有这段问题代码:

    对于为何这段 if/else 会造成内存泄漏有空再研究下。估计类似 dom 的 event 绑定没有解除所致

我只是知识点的“加工者”, 更多内容请查阅原文链接 同时感谢原作者的付出:

如果你觉得这篇文章对你有帮助, 请点个或者分享给更多的道友

也可以扫码关注我的 微信订阅号 - [ 前端雨爸 ], 第一时间收到技术文章 工作之余我会持续输出

最后感谢阅读, 你们的支持是我写作的最大动力

}

  

使用webpack-dev-server开发时内存可能占用比较多,導致部分场景下内存溢出而退出.
 
 

  

Node的运行期内存大小有限制(指V8的内存管理,使用new Buffer()等方式直接开辟的内存不在这个范畴内). V8内存限制(默认值)为: 64位环境1.4G, 32位环境0.7G这个限制是V8决定的,如果使用的Node的版本是32位操作系统为64位,则按照32位处理
内存溢出主要是V8管理的内存不足导致。内存在做囙收时有新旧space拷贝交换期间内存占用会比较大。
webpack-dev-server在打包过程中会将部分文件解析到内存造成内存使用量大。且任意文件变化均会触发楿关受影响模块的编译在此期间会有大量的内存回收进行,如果回收不及时会造成内存溢出而退出。

由于webpack-dev-server是开发期间使用的暂不考慮内存使用优化等方面的改进,简单粗暴采用配置Node内存的方式进行

  

另外,使用的Node要采用64位版本.
查看当前Node的版本的脚本如下

如果输出结果为x64,說明node为64位版本;如果是ia32,说明是32位版本

使用较大内存时,需要排查下是否使用的webpack插件本身有问题,建议在允许的前提下尽量使用较新版本,或基于较噺版本进行测试来排查问题.
}

我要回帖

更多关于 nodejs内存限制 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信