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

智能推荐

分布式光纤传感器的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告_预计2026年中国分布式传感器市场规模有多大-程序员宅基地

文章浏览阅读3.2k次。本文研究全球与中国市场分布式光纤传感器的发展现状及未来发展趋势,分别从生产和消费的角度分析分布式光纤传感器的主要生产地区、主要消费地区以及主要的生产商。重点分析全球与中国市场的主要厂商产品特点、产品规格、不同规格产品的价格、产量、产值及全球和中国市场主要生产商的市场份额。主要生产商包括:FISO TechnologiesBrugg KabelSensor HighwayOmnisensAFL GlobalQinetiQ GroupLockheed MartinOSENSA Innovati_预计2026年中国分布式传感器市场规模有多大

07_08 常用组合逻辑电路结构——为IC设计的延时估计铺垫_基4布斯算法代码-程序员宅基地

文章浏览阅读1.1k次,点赞2次,收藏12次。常用组合逻辑电路结构——为IC设计的延时估计铺垫学习目的:估计模块间的delay,确保写的代码的timing 综合能给到多少HZ,以满足需求!_基4布斯算法代码

OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版-程序员宅基地

文章浏览阅读3.3k次,点赞3次,收藏5次。OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版

关于美国计算机奥赛USACO,你想知道的都在这_usaco可以多次提交吗-程序员宅基地

文章浏览阅读2.2k次。USACO自1992年举办,到目前为止已经举办了27届,目的是为了帮助美国信息学国家队选拔IOI的队员,目前逐渐发展为全球热门的线上赛事,成为美国大学申请条件下,含金量相当高的官方竞赛。USACO的比赛成绩可以助力计算机专业留学,越来越多的学生进入了康奈尔,麻省理工,普林斯顿,哈佛和耶鲁等大学,这些同学的共同点是他们都参加了美国计算机科学竞赛(USACO),并且取得过非常好的成绩。适合参赛人群USACO适合国内在读学生有意向申请美国大学的或者想锻炼自己编程能力的同学,高三学生也可以参加12月的第_usaco可以多次提交吗

MySQL存储过程和自定义函数_mysql自定义函数和存储过程-程序员宅基地

文章浏览阅读394次。1.1 存储程序1.2 创建存储过程1.3 创建自定义函数1.3.1 示例1.4 自定义函数和存储过程的区别1.5 变量的使用1.6 定义条件和处理程序1.6.1 定义条件1.6.1.1 示例1.6.2 定义处理程序1.6.2.1 示例1.7 光标的使用1.7.1 声明光标1.7.2 打开光标1.7.3 使用光标1.7.4 关闭光标1.8 流程控制的使用1.8.1 IF语句1.8.2 CASE语句1.8.3 LOOP语句1.8.4 LEAVE语句1.8.5 ITERATE语句1.8.6 REPEAT语句。_mysql自定义函数和存储过程

半导体基础知识与PN结_本征半导体电流为0-程序员宅基地

文章浏览阅读188次。半导体二极管——集成电路最小组成单元。_本征半导体电流为0

随便推点

【Unity3d Shader】水面和岩浆效果_unity 岩浆shader-程序员宅基地

文章浏览阅读2.8k次,点赞3次,收藏18次。游戏水面特效实现方式太多。咱们这边介绍的是一最简单的UV动画(无顶点位移),整个mesh由4个顶点构成。实现了水面效果(左图),不动代码稍微修改下参数和贴图可以实现岩浆效果(右图)。有要思路是1,uv按时间去做正弦波移动2,在1的基础上加个凹凸图混合uv3,在1、2的基础上加个水流方向4,加上对雾效的支持,如没必要请自行删除雾效代码(把包含fog的几行代码删除)S..._unity 岩浆shader

广义线性模型——Logistic回归模型(1)_广义线性回归模型-程序员宅基地

文章浏览阅读5k次。广义线性模型是线性模型的扩展,它通过连接函数建立响应变量的数学期望值与线性组合的预测变量之间的关系。广义线性模型拟合的形式为:其中g(μY)是条件均值的函数(称为连接函数)。另外,你可放松Y为正态分布的假设,改为Y 服从指数分布族中的一种分布即可。设定好连接函数和概率分布后,便可以通过最大似然估计的多次迭代推导出各参数值。在大部分情况下,线性模型就可以通过一系列连续型或类别型预测变量来预测正态分布的响应变量的工作。但是,有时候我们要进行非正态因变量的分析,例如:(1)类别型.._广义线性回归模型

HTML+CSS大作业 环境网页设计与实现(垃圾分类) web前端开发技术 web课程设计 网页规划与设计_垃圾分类网页设计目标怎么写-程序员宅基地

文章浏览阅读69次。环境保护、 保护地球、 校园环保、垃圾分类、绿色家园、等网站的设计与制作。 总结了一些学生网页制作的经验:一般的网页需要融入以下知识点:div+css布局、浮动、定位、高级css、表格、表单及验证、js轮播图、音频 视频 Flash的应用、ul li、下拉导航栏、鼠标划过效果等知识点,网页的风格主题也很全面:如爱好、风景、校园、美食、动漫、游戏、咖啡、音乐、家乡、电影、名人、商城以及个人主页等主题,学生、新手可参考下方页面的布局和设计和HTML源码(有用点赞△) 一套A+的网_垃圾分类网页设计目标怎么写

C# .Net 发布后,把dll全部放在一个文件夹中,让软件目录更整洁_.net dll 全局目录-程序员宅基地

文章浏览阅读614次,点赞7次,收藏11次。之前找到一个修改 exe 中 DLL地址 的方法, 不太好使,虽然能正确启动, 但无法改变 exe 的工作目录,这就影响了.Net 中很多获取 exe 执行目录来拼接的地址 ( 相对路径 ),比如 wwwroot 和 代码中相对目录还有一些复制到目录的普通文件 等等,它们的地址都会指向原来 exe 的目录, 而不是自定义的 “lib” 目录,根本原因就是没有修改 exe 的工作目录这次来搞一个启动程序,把 .net 的所有东西都放在一个文件夹,在文件夹同级的目录制作一个 exe._.net dll 全局目录

BRIEF特征点描述算法_breif description calculation 特征点-程序员宅基地

文章浏览阅读1.5k次。本文为转载,原博客地址:http://blog.csdn.net/hujingshuang/article/details/46910259简介 BRIEF是2010年的一篇名为《BRIEF:Binary Robust Independent Elementary Features》的文章中提出,BRIEF是对已检测到的特征点进行描述,它是一种二进制编码的描述子,摈弃了利用区域灰度..._breif description calculation 特征点

房屋租赁管理系统的设计和实现,SpringBoot计算机毕业设计论文_基于spring boot的房屋租赁系统论文-程序员宅基地

文章浏览阅读4.1k次,点赞21次,收藏79次。本文是《基于SpringBoot的房屋租赁管理系统》的配套原创说明文档,可以给应届毕业生提供格式撰写参考,也可以给开发类似系统的朋友们提供功能业务设计思路。_基于spring boot的房屋租赁系统论文