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


Posted in Javascript onJune 23, 2015

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

我们将从一篇关于Node.js的实质性问题:内存泄漏的主题文章开始。我们会介绍 node-memwatch — 一个帮助发现并隔离Node中的内存泄漏问题的函数库。

为什么自寻烦恼?

关于追踪内存泄漏问得最多的问题就是,“为什么要自寻烦恼?”。难道没有更紧迫的问题需要先解决吗?为什么不选择不时地重启服务,或为之分配更多的RAM?为了回答这些问题,我们提出了以下三点建议:

1.也许你不在乎不断增长的内存占用,但V8在乎(V8是Node运行时的引擎)。随着内存泄漏的增长,V8对垃圾收集器越来越具有攻击性,这会使你的应用运行速度变慢。所以,在Node上,内存泄漏会损害程序性能。

2.内存泄漏可能触发其他类型的失败。内存泄漏的代码可能会持续的引用有限的资源。你可能会耗尽文件描述符;你还可能会突然不能建立新的数据库连接。这类问题可能在你的应用耗尽内存前很早就会暴露出来,但它仍然会是你陷入困境。

3.最后,你的应用迟早会崩溃,并且在你的应用受到欢迎时肯定会发生。所有人都会在Hacker News上嘲笑你,讽刺你,这样你就悲剧了。

溃千里之堤的蚁穴在哪里?

在构建复杂应用的时候,很多地方都可能发生内存泄露。 闭包可能是最广为人知也是最声名狼藉的。因为闭包保留了对其作用域内的东西的引用,而这正是通常的内存泄露之源。

闭包泄露往往只有在有人去寻找它们的时候才能发现。但是在Node的异步世界里,我们随时随地的通过回调函数不停的生成闭包。如果这些回调函数没有在创建后立刻使用,分配的内存就会持续增长,那些看起来没有内存泄露问题的代码也会产生泄露。而这种问题更难发现。

你的应用也可能由于上游代码的问题导致内存泄露。也许你能定位到出现内存泄露的代码,但是你可能只能眼巴巴地盯着你那完美无缺的代码然后困惑于这到底是怎么泄露的!

正是这些难以定位的内存泄露促使我们想要一个node-memwatch这样的工具。传说几个月以前,我们的Lloyd Hilaiel把他自己锁在一个小房间里两天,试着追踪一个在压力测试下变得非常明显的内存泄露问题。(顺便说下,尽请期待Lloyd即将到来的关于负荷测试的文章)

经过两天的努力,他终于发现了Node内核中的元凶:http.ClientRequest中的事件监听器没有被释放。(最终修复这个问题的补丁只有两个但却至关重要的字母)。正是这次痛苦的经历促使Lloyd想要写一个能够帮助查找内存泄露的工具。

内存泄露定位工具

现在已经有许多好用且不断增强的工具用于定位Node.js应用的内存泄露。下面是其中的一些:

  •     Jimb Esser的node-mtrace,它使用了GCC的mtrace工具来分析堆的使用。
  •     Dave Pacheco的node-heap-dump对V8的堆抓取了一张快照并把所有的东西序列化进一个巨大的JSON文件。它还包含了一些分析研究快照结果的JavaScript工具。
  •     Danny Coates的v8-profiler和node-inspector提供了绑定在Node中的V8分析器和一个基于WebKit Web Inspector的debug界面。
  •     Felix Gnass的未禁用保持器图表分支。
  •     Felix Geisendorfer的Node内存泄露指导(Node Memory Leak Tutorial)是一个又短又酷的v8-profiler和node-debugger使用教程。同时也是目前最先进的Node.js内存泄露调试技术指南。
  •     Joyent的SmartOS平台,它提供了大量用于调试Node.js内存泄露的工具。

上面的这些工具我们都很喜欢,但是没有一个适用于我们的场景。Web Inspector对于开发中的应用非常棒,但是很难用于热部署的场景,尤其是在多服务器和涉及子进程的时候。同样的,在长时间高负载运行中出现的内存泄露也很难复现。像dtrace和libumem这样的工具虽然让人印象深刻,但是不是所有的操作系统都能用。

Enternode-memwatch

我们需要一个跨平台的调试库,当我们的程序可能存在内存泄漏时,它不需要设备告诉我们,并且会帮我们找到哪里存在泄漏。所以我们实现了node-memwatch。

它给我们提供三件东西:

    一个‘泄漏'事件发射器
   

memwatch.on('leak', function(info) {
  // look at info to find out about what might be leaking
  });

    一个‘状态事件发射器
   
     

var memwatch = require('memwatch');
  memwatch.on('stats', function(stats) {
  // do something with post-gc memory usage stats
  });

    一个堆内存区分类
   

var hd = new memwatch.HeapDiff();
  // your code here ...
  var diff = hd.end();

    并且还有一个在测试时很有用处的,可以触发垃圾收集器的功能。好吧,一共四点。
   
  

var stats = memwatch.gc();

memwatch.on('stats', ...): Post-GC堆统计

node-memwatch能够在任何一个JS对象分配之前,紧随着一次完整的垃圾回收和内存压缩发出一个内存使用样本。(它使用了V8的post-gc钩子,V8::AddGCEpilogueCallback,来在每次垃圾回收触发时收集堆使用信息)

统计数据包括:

  •     usage_trend(使用趋势)
  •     current_base(当前基数)
  •     estimated_base(预期基数)
  •     num_full_gc (完整的垃圾回收次数)
  •     num_inc_gc (增长的垃圾回收次数)
  •     heap_compactions (内存压缩次数)
  •     min (最小)
  •     max (最大)

这里有一个展示存在内存泄露的应用的数据看起来是什么样的例子。下面的图表随着时间追踪内存的使用。疯狂的绿线展示了process.memoryUsage()报告的内容。红线展示了node_memwatch报告的current_base。左下侧的盒子展示了附加信息。

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

 注意Incr GCs非常高。那说明V8在拼命的尝试清理内存。

memwatch.on('leak', ...): 堆分配趋势

我们定义了一个简单的侦测算法来提醒你应用程序可能存在内存泄漏。即如果经过连续五次GC,内存仍被持续分配而没有得到释放,node-memwatch就会发出一个leak事件。事件的具体信息格式是明了易读的,就像这样:
 

{ start: Fri, 29 Jun 2012 14:12:13 GMT,
 end: Fri, 29 Jun 2012 14:12:33 GMT,
 growth: 67984,
 reason: 'heap growth over 5 consecutive GCs (20s) - 11.67 mb/hr' }

memwatch.HeapDiff(): 查找泄漏元凶

最后,node-memwatch能比较堆上对象的名称和分配数量的快照,其对比前后的差异可以帮助找出导致内存泄漏的元凶。
 

var hd = new memwatch.HeapDiff();
 
// Your code here ...
 
var diff = hd.end();

对比产生的内容就像这样:
 

{
 "before": {
  "nodes": 11625,
  "size_bytes": 1869904,
  "size": "1.78 mb"
 },
 "after": {
  "nodes": 21435,
  "size_bytes": 2119136,
  "size": "2.02 mb"
 },
 "change": {
  "size_bytes": 249232,
  "size": "243.39 kb",
  "freed_nodes": 197,
  "allocated_nodes": 10007,
  "details": [
   {
    "what": "Array",
    "size_bytes": 66688,
    "size": "65.13 kb",
    "+": 4,
    "-": 78
   },
   {
    "what": "Code",
    "size_bytes": -55296,
    "size": "-54 kb",
    "+": 1,
    "-": 57
   },
   {
    "what": "LeakingClass",
    "size_bytes": 239952,
    "size": "234.33 kb",
    "+": 9998,
    "-": 0
   },
   {
    "what": "String",
    "size_bytes": -2120,
    "size": "-2.07 kb",
    "+": 3,
    "-": 62
   }
  ]
 }
}

HeapDiff方法在进行数据采样前会先进行一次完整的垃圾回收,以使得到的数据不会充满太多无用的信息。memwatch的事件处理会忽略掉由HeapDiff触发的垃圾回收事件,所以在stats事件的监听回调函数中你可以安全地调用HeapDiff方法。

Javascript 相关文章推荐
MSN消息提示类
Sep 05 Javascript
js中call与apply的用法小结
Dec 28 Javascript
解决jquery中动态新增的元素节点无法触发事件问题的两种方法
Oct 30 Javascript
详解Javacript和AngularJS中的Promises
Feb 09 Javascript
jQuery多级联动下拉插件chained用法示例
Aug 20 Javascript
jquery拼接ajax 的json和字符串拼接的方法
Mar 11 Javascript
ejsExcel模板在Vue.js项目中的实际运用
Jan 27 Javascript
小程序登录态管理的方法示例
Nov 13 Javascript
一个因@click.stop引发的bug的解决
Jan 08 Javascript
详解Vue后台管理系统开发日常总结(组件PageHeader)
Nov 01 Javascript
vue 解决移动端弹出键盘导致页面fixed布局错乱的问题
Nov 06 Javascript
如何理解Vue前后端数据交互与显示
May 10 Vue.js
充分发挥Node.js程序性能的一些方法介绍
Jun 23 #Javascript
Node.js编程中客户端Session的使用详解
Jun 23 #Javascript
使用Meteor配合Node.js编写实时聊天应用的范例
Jun 23 #Javascript
使用Node.js为其他程序编写扩展的基本方法
Jun 23 #Javascript
Windows系统下Node.js的简单入门教程
Jun 23 #Javascript
jQuery实现判断滚动条到底部
Jun 23 #Javascript
jQuery实现新消息在网页标题闪烁提示
Jun 23 #Javascript
You might like
PHP5.0对象模型探索之抽象方法和抽象类
2006/09/05 PHP
一个很不错的PHP翻页类
2009/06/01 PHP
PHP微信开发用Cache 解决数据缓存
2016/07/11 PHP
php中10个不同等级压缩优化图片操作示例
2016/11/14 PHP
详解yii2使用多个数据库的案例
2017/06/16 PHP
纯js实现瀑布流展现照片(自动适应窗口大小)
2013/04/08 Javascript
js控制页面的全屏展示和退出全屏显示的方法
2015/03/10 Javascript
nodejs开发微博实例
2015/03/25 NodeJs
JavaScript中的Function函数
2015/08/27 Javascript
微信小程序 实现点击添加移除class
2017/06/12 Javascript
微信小程序中使用echarts的实现方法
2019/04/24 Javascript
详解js根据百度地图提供经纬度计算两点距离
2019/05/13 Javascript
vue-cli脚手架引入弹出层layer插件的几种方法
2019/06/24 Javascript
浅谈目前可以使用ES10的5个新特性
2019/06/25 Javascript
JavaScript ES 模块的使用
2020/11/12 Javascript
python中__call__方法示例分析
2014/10/11 Python
Python PyQt5标准对话框用法示例
2017/08/23 Python
Python3对称加密算法AES、DES3实例详解
2018/12/06 Python
python读取图片的方式,以及将图片以三维数组的形式输出方法
2019/07/03 Python
详解解决Python memory error的问题(四种解决方案)
2019/08/08 Python
解析python实现Lasso回归
2019/09/11 Python
使用Python内置模块与函数进行不同进制的数的转换
2020/04/26 Python
Python Matplotlib简易教程(小白教程)
2020/07/28 Python
python 用opencv实现霍夫线变换
2020/11/27 Python
城市观光通行证:The Sightseeing Pass
2018/04/28 全球购物
荷兰网上药店:Drogisterij.net
2019/09/03 全球购物
简述网络文件系统NFS,并说明其作用
2016/10/19 面试题
Linux管理员面试经常问道的相关命令
2014/12/12 面试题
住房租房协议书
2014/08/20 职场文书
2014年教育工作总结
2014/11/26 职场文书
乔迁之喜答谢词
2015/01/05 职场文书
培养联系人考察意见
2015/06/01 职场文书
员工规章制度范本
2015/08/07 职场文书
MySQL令人大跌眼镜的隐式转换
2021/08/23 MySQL
PostGIS的安装与入门使用指南
2022/01/18 PostgreSQL
AngularJS实现多级下拉框
2022/03/25 Javascript