Node.js学习1_记一次 Node.js 应用内存暴涨分析_js使用内存一直上升的原因-程序员宅基地

技术标签: 转载  node.js  CPU  Node.js  

起因

之前 TMS 在运行时 CPU 中占用率和内存占用一直很高,导致应用运行状态不是很良好,需要频繁重启。经过排查,找出了部分原因:

  1. 使用的 html-minifier 模块有问题,如果输入的内容是一个有错误的 HTML 结构,会使解析进入死循环,导致 CPU 占用率 100%。

  2. 在使用 vm 模块时,使用姿势错误,导致内存占用无法释放,使内存占用暴涨。

第一个问题我们今天不予讨论,主要来说一下第二个问题。

VM(Virtual Machine) 模块

我们就先了解下 VM 这个模块。

从它的名字和暴露的 API 可以看出,它能创建一个拥有指定上下文的运行环境,可以在里面直接运行 JavaScript 代码,类似 eval。这样运行代码时,不会污染当前作用域,一旦出问题,也不会对当前环境造成很大影响。

虽然这个模块我们平时用的比较少,但它算是 Node.js 的核心模块,在 require 的实现中,你会发现它的身影。我们在使用 Node.js 时,会使用 require 引入很多外部模块,对于 Node.js 来说,我们引入的代码如果直接和运行环境交互,是十分危险的。所以在 Node.js 模块加载的过程中,会先将 .js 文件的内容进行包裹,变成类似 function(...) {}(...) 的形式,然后使用 vm.runInThisContext 去运行,同时将 module、require 等方法传入返回的函数中。具体的模块加载机制,可以在 lib/module.js 中看到实现,不是本文重点,就不细说了。

当然,我们也可以用它来执行我们的代码:

 
const vm = require('vm');
const code = 'result = 2 * n;';
const script = new vm.Script(code); // 预编译后供之后使用
const sandbox = { n: 5 };
const _sandbox = { n: 10 };
const ctx = vm.createContext(_sandbox); // contextify

// 供 runInThisContext 使用
global.result = 0;
global.n = 16;
// 在当前上下文运行,32
vm.runInThisContext(code);
script.runInThisContext();
// 在新的上下文中运行,10
vm.runInNewContext(code, sandbox);
script.runInNewContext(sandbox);
// 在执行上下文中运行,20
vm.runInContext(code, _sandbox);
script.runInContext(_sandbox);

问题出现

在 TMS 中,需要压缩用户上传的代码,出于安全和稳定的考虑,需要和当前运行环境进行隔离,这里就可以使用 VM 模块。为了便于理解,简化了一个类似的 Demo,如下:

 
// fibonacci,计算斐波纳挈数列
http.createServer(function(req, res) {
       
  let sandbox = {
       
    fibonacci: fibonacci,
    number: 10
  };

  vm.runInNewContext('a = fibonacci(number)', sandbox);
  res.end();
}).listen(8999, '127.0.0.1');

运行 Demo。为了模拟实际环境中的并发,这里我们使用 ab 来发起请求。

 
ab -n 1000 -c 100  http://127.0.0.1:8999/

Apache HTTP server benchmarking tool,简称 ab,是一个常用的开源网站压力测试工具,官网

在运行期间,我们使用 top 来观察内存的占用情况。

实验一

可以发现一些问题,

  • 内存占用暴涨,大约 800M
  • 占用的内存在运行结束(没有请求)后,释放很慢
  • QPS 很低

Demo 应用比较简单,引发的问题不大。但如果在实际的应用场景中,一旦发生内存占用过高,无法分配内存空间的情况,会对应用稳定性照成很大影响,甚至导致应用崩溃。

接下来,我们再看一个例子,将上面的代码稍作修改,如下:

 
let sandbox = {
       
  fibonacci: fibonacci,
  number: 10
};

http.createServer(function server (req, res) {
       
  vm.runInNewContext('a = fibonacci(number)', sandbox);
  res.end();
}).listen(8999, '127.0.0.1');

用上面同样的方法观察,结果如下图:

实验二

这次,我们看到内存仅占用了 19M,而且增长很平缓,QPS 提高了不少。

仅仅是声明 sandbox 位置的不同,差别却如此之大,为什么呢?

探究原因

我们都知道,一般一个在函数中声明的变量,在函数运行完,就会被释放掉,所占用的空间也会被回收。但在之前的例子,很有可能 sandbox 变量没有被回收,导致的内存暴涨。它和其它变量有什么区别,导致它不能被正确释放呢?

翻了下 vm 的代码,发现在使用 vm.runInNewContext 时,会将你传入的 sandbox 进行 contextify,问题可能就出在这里。

contextify 大体流程如下(src/node_contextify.cc#L281 MakeContext):

  1. 检查传入的对象(sandbox)是否有 _contextifyHidden 这个隐藏的属性。
  2. 如果没有,则 new 一个 ContextifyContext 实例,并且挂载到 sandbox 的_contextifyHidden 属性上。
  3. 如果存在,则返回,不做处理,防止在一个对象上多次进行 contextify。

如果我们用一个在函数外部声明的 sandbox,如同第二种写法,那么无论我们调用多少次 runInNewContext,都只会进行一次 contextify 操作,效果类似于 vm.runInContext。但是,如果像第一种写法那样,每次都使用一个新的对象,那么每次都要进行 contextify,而 contextify 过程中比较关键的一步是创建一个 ContextifyContext 实例,这个类有些特殊的地方,我们看下它的具体定义(在 src/node_contextify.cc#L49 ):

 
class ContextifyContext {
       
  ...
  Persistent<Object> sandbox_;
  Persistent<Context> context_;
  Persistent<Object> proxy_global_;
  ...
  public:
    explicit ContextifyContext(Environment* env, Local<Object> sandbox) {
       
      ...     
      sandbox_.MarkIndependent();
      ...
      context_.MarkIndependent();
      ...
      proxy_global_.MarkIndependent();
      ...
    }
    ...
}

它里面有三个被声明成 Persistent 类型的变量,重点就在这里。

在 V8 中,有三种概念, Handle 、 Local 、 Persistent 。所有 JavaScript 数据都是有 GC 管理的。JavaScript 中的变量在 C++ 层面都是和 Handle 对应的,可以把它理解成一个普通的指针,用来指向数据的内存位置。而 Local 可以看做一个实际存储数据的空间,拥有 new 方法,当它 new 出来后,无论是否有变量接收,都会存在于 HandleScope中。 HandleScope 可以理解成一个管理和回收 Handle 的东西。所以,一个 Local 可以有多个 Handle 指向。而 HandleScope 类似于函数的作用域,它管理着 Handle 和 Local,一旦 HandleScope 退出,其上的 Handle 和 Local 就会被释放掉,可以联系 JavaScript 中的函数作用域来理解。

如同 JavaScript 中的闭包一样,我们有时会需要一种在函数退出后依然存在的变量,这就是 Persistent 类型,它不由 HandleScope 管理,只要没有手动释放,它就一直可以被使用。可以简单用堆和栈的概念来理解,Persistent 是堆变量,HandleScope 是栈,Local 是栈变量,而 Handle 是一种引用。

对于 Persistent 类型变量,除了手动调用 Dispose() 释放外,V8 还提供了一种自动的,依赖 GC 的释放方式,就是 Weak Callback + MarkIndependent 的组合,显然,Node.js 就是使用的这种。这种方式的优势在于自动化,不用开发者去管理这部分内存,但是过分的依赖 GC,难免会产生各种各样的问题,比如:内存释放不及时,占用过多系统资源等。

要知道,GC 并不是实时的,它是需要程序停下来一段时间来让它来进行回收操作的,如果程序一直在运行,那么 GC 操作就会被延后,直到它觉得必需要运行的时候。这样,会造成要释放资源的积压。如果频繁执行 GC,则会影响程序的运行效率。

而且,Weak Callback 的执行是由 GC 决定的,一般是在 Full GC 前后。比较过分的是,GC 不保证一定会调用这个回调。。。

另外,在上述的场景中,通过试验,可以做这样的猜想:因为 old space 默认大小为 1G,而我们看到在 1000 次执行完后,old space 才 800M 左右,没有达到阈值,所以 V8 并不会处理这部分的内存占用。当我们把 old space 设为 200M 时,其值稳定在 180M 左右,可以大体印证这个猜想。

综上,问题的根源找到了。每次请求回调里都会创建一个新的 sandbox,并且它不能在使用完后立即释放,于是就形成很多无用的 Persistent Handle,堆积在内存中,导致内存占用暴涨。而且,它们的释放主要依赖于 MarkSweep,执行频率不高,所以占用释放很慢。可以想象,在一个高 QPS 的应用下,内存基本上是只增不降的,一点点被蚕食干净。

解决问题

问题既然找到了,那么就来看下如何解决。

方案一

把 sandbox 在回调外面声明,减少重复 contextify。因为脚本运行所需要的 context 对象实际上就是 sandbox 对象,只是在底层标识了一下(_contextifyHidden),这一点在 MakeContext 函数中以及获取 vm 里的返回值时可以看出来,所以修改 sandbox 的值即可以实现传递不同参数的效果。

 
let sandbox = {
       
  fibonacci: fibonacci,
  number: 10
};

http.createServer(function(req, res) {
       
  // 传递不同的值
  sandbox.number = Math.floor(Math.random() * 20);
  vm.runInNewContext('a = fibonacci(number)', sandbox);
  res.end();
}).listen(8999, '127.0.0.1');

方案二

vm 模块本身提供了复用的能力,Script 和 createContext,所以可以利用它们来处理。

 
const code = 'a = fibonacci(number)';
const script = new vm.Script(code);
let sandbox = {
       
  fibonacci: fibonacci,
  number: 10
};
let ctx = vm.createContext(sandbox);

http.createServer(function(req, res) {
       
  sandbox.number = Math.floor(Math.random() * 20);
  script.runInContext(ctx);
  res.end();
}).listen(8999, '127.0.0.1');

从上面 contextify 的过程中,我们除了可以发现 context 和 sandbox 是关联的之外,还有一点就是 runInNewContext 会对 sandbox 做校验,所以这里使用 runInNewContext 也不会有上述的问题。

方案三

这种方案更有普适性,不一定针对于这个问题本身。

Node.js 本身提供了很多关于 GC 方面的参数。

MarkSweep,Full GC 的标记阶段

  • --trace_gc,打印 GC 日志
  • --expose-gc,暴露 GC 方法,可以手动调用 global.gc() 来强制执行 GC 过程,并不推荐使用。
  • --max-new-space-size,最大 new space 大小,执行 scavenge 回收,默认 16M,单位 KB
  • --max-old-space-size,最大 old space 大小,执行 MarkSweep 回收,默认 1G,单位 MB
  • --gc-global,强制每次执行 MarkSweep。

可以通过调节这些参数的配置,观察 GC 日志中 sweeping from(内存积压状况)、Mark-sweep(MarkSweep 用时)等,来优化 GC 过程,需要一定的耐心。当然,有些值不能太极端,比如把 --max-old-space-size 设置的很小,频繁触发 GC,会导致应用的执行效率下降。

以后如何发现问题

以后如果遇到一些性能问题,我们该如何去排查呢?这里介绍一些常用的方法。

v8 prof

使用 V8 自带的 profiler 功能,分析 JavaScript 各个函数的消耗和 GC 部分。

 
npm install profiler
node --prof xxx.js

会生成 xxxx-v8.log,之后使用工具转换成可读的。

 
npm install tick
node-tick-processor xxxx-v8.log

就可以查看相关的数据了。

node-inspector

这个工具就不多介绍了,大家应该很熟了,它可以使用 Chrome 开发者工具来调试 Node.js 应用。

node-heapdump

它可以对 Node.js 应用进行 heapdump。然后,可以使用 Chrome 开发者工具打开生成的 xxx.heapsnapshoot 文件,查看 heap 中的内容。

 
npm install heapdump

在应用中引入

 
var heapdump = require('heapdump');

执行一段时间后退出,或者在命令行中:

 
kill -USR2 <pid>

v8-profiler

这个被 node-inspector 集成了,可以提供 HeapDump 和 CPU Profile 功能。
详见 v8-profiler

node-memwatch

可以帮助发现代码存在的内存泄露问题,也可以做在不同时间点堆的比较。
详见 node-memwatch

当然,工具只是辅助作用,在平时写代码时多思考一下,善用 API,在处理问题时多积累些经验,才能写出更好的代码。

总结

V8 提供的内存释放方案有它的优势所在,但 GC 是个很复杂的过程,过分依赖自动化,也不一定是好事。特别在写 Node.js 底层的 C++ 部分时,我们还是要考虑下是否该手动释放的问题,不要把问题都抛给 V8。当然,对于 API 应用也要注意,本身 VM 模块提供了更好的方案,但我们却忽略了。

V8 比较复杂,理解有误的地方,欢迎指正,讨论。

参考资料:

原文来自:

http://taobaofed.org/blog/2016/01/14/nodejs-memory-leak-analyze/

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/wang_zhenwei/article/details/50521581

智能推荐

使用nginx解决浏览器跨域问题_nginx不停的xhr-程序员宅基地

文章浏览阅读1k次。通过使用ajax方法跨域请求是浏览器所不允许的,浏览器出于安全考虑是禁止的。警告信息如下:不过jQuery对跨域问题也有解决方案,使用jsonp的方式解决,方法如下:$.ajax({ async:false, url: 'http://www.mysite.com/demo.do', // 跨域URL ty..._nginx不停的xhr

在 Oracle 中配置 extproc 以访问 ST_Geometry-程序员宅基地

文章浏览阅读2k次。关于在 Oracle 中配置 extproc 以访问 ST_Geometry,也就是我们所说的 使用空间SQL 的方法,官方文档链接如下。http://desktop.arcgis.com/zh-cn/arcmap/latest/manage-data/gdbs-in-oracle/configure-oracle-extproc.htm其实简单总结一下,主要就分为以下几个步骤。..._extproc

Linux C++ gbk转为utf-8_linux c++ gbk->utf8-程序员宅基地

文章浏览阅读1.5w次。linux下没有上面的两个函数,需要使用函数 mbstowcs和wcstombsmbstowcs将多字节编码转换为宽字节编码wcstombs将宽字节编码转换为多字节编码这两个函数,转换过程中受到系统编码类型的影响,需要通过设置来设定转换前和转换后的编码类型。通过函数setlocale进行系统编码的设置。linux下输入命名locale -a查看系统支持的编码_linux c++ gbk->utf8

IMP-00009: 导出文件异常结束-程序员宅基地

文章浏览阅读750次。今天准备从生产库向测试库进行数据导入,结果在imp导入的时候遇到“ IMP-00009:导出文件异常结束” 错误,google一下,发现可能有如下原因导致imp的数据太大,没有写buffer和commit两个数据库字符集不同从低版本exp的dmp文件,向高版本imp导出的dmp文件出错传输dmp文件时,文件损坏解决办法:imp时指定..._imp-00009导出文件异常结束

python程序员需要深入掌握的技能_Python用数据说明程序员需要掌握的技能-程序员宅基地

文章浏览阅读143次。当下是一个大数据的时代,各个行业都离不开数据的支持。因此,网络爬虫就应运而生。网络爬虫当下最为火热的是Python,Python开发爬虫相对简单,而且功能库相当完善,力压众多开发语言。本次教程我们爬取前程无忧的招聘信息来分析Python程序员需要掌握那些编程技术。首先在谷歌浏览器打开前程无忧的首页,按F12打开浏览器的开发者工具。浏览器开发者工具是用于捕捉网站的请求信息,通过分析请求信息可以了解请..._初级python程序员能力要求

Spring @Service生成bean名称的规则(当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致)_@service beanname-程序员宅基地

文章浏览阅读7.6k次,点赞2次,收藏6次。@Service标注的bean,类名:ABDemoService查看源码后发现,原来是经过一个特殊处理:当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致public class AnnotationBeanNameGenerator implements BeanNameGenerator { private static final String C..._@service beanname

随便推点

二叉树的各种创建方法_二叉树的建立-程序员宅基地

文章浏览阅读6.9w次,点赞73次,收藏463次。1.前序创建#include&lt;stdio.h&gt;#include&lt;string.h&gt;#include&lt;stdlib.h&gt;#include&lt;malloc.h&gt;#include&lt;iostream&gt;#include&lt;stack&gt;#include&lt;queue&gt;using namespace std;typed_二叉树的建立

解决asp.net导出excel时中文文件名乱码_asp.net utf8 导出中文字符乱码-程序员宅基地

文章浏览阅读7.1k次。在Asp.net上使用Excel导出功能,如果文件名出现中文,便会以乱码视之。 解决方法: fileName = HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8);_asp.net utf8 导出中文字符乱码

笔记-编译原理-实验一-词法分析器设计_对pl/0作以下修改扩充。增加单词-程序员宅基地

文章浏览阅读2.1k次,点赞4次,收藏23次。第一次实验 词法分析实验报告设计思想词法分析的主要任务是根据文法的词汇表以及对应约定的编码进行一定的识别,找出文件中所有的合法的单词,并给出一定的信息作为最后的结果,用于后续语法分析程序的使用;本实验针对 PL/0 语言 的文法、词汇表编写一个词法分析程序,对于每个单词根据词汇表输出: (单词种类, 单词的值) 二元对。词汇表:种别编码单词符号助记符0beginb..._对pl/0作以下修改扩充。增加单词

android adb shell 权限,android adb shell权限被拒绝-程序员宅基地

文章浏览阅读773次。我在使用adb.exe时遇到了麻烦.我想使用与bash相同的adb.exe shell提示符,所以我决定更改默认的bash二进制文件(当然二进制文件是交叉编译的,一切都很完美)更改bash二进制文件遵循以下顺序> adb remount> adb push bash / system / bin /> adb shell> cd / system / bin> chm..._adb shell mv 权限

投影仪-相机标定_相机-投影仪标定-程序员宅基地

文章浏览阅读6.8k次,点赞12次,收藏125次。1. 单目相机标定引言相机标定已经研究多年,标定的算法可以分为基于摄影测量的标定和自标定。其中,应用最为广泛的还是张正友标定法。这是一种简单灵活、高鲁棒性、低成本的相机标定算法。仅需要一台相机和一块平面标定板构建相机标定系统,在标定过程中,相机拍摄多个角度下(至少两个角度,推荐10~20个角度)的标定板图像(相机和标定板都可以移动),即可对相机的内外参数进行标定。下面介绍张氏标定法(以下也这么称呼)的原理。原理相机模型和单应矩阵相机标定,就是对相机的内外参数进行计算的过程,从而得到物体到图像的投影_相机-投影仪标定

Wayland架构、渲染、硬件支持-程序员宅基地

文章浏览阅读2.2k次。文章目录Wayland 架构Wayland 渲染Wayland的 硬件支持简 述: 翻译一篇关于和 wayland 有关的技术文章, 其英文标题为Wayland Architecture .Wayland 架构若是想要更好的理解 Wayland 架构及其与 X (X11 or X Window System) 结构;一种很好的方法是将事件从输入设备就开始跟踪, 查看期间所有的屏幕上出现的变化。这就是我们现在对 X 的理解。 内核是从一个输入设备中获取一个事件,并通过 evdev 输入_wayland

推荐文章

热门文章

相关标签