浅析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 相关文章推荐
js判断浏览器的比较全的代码
Feb 13 Javascript
在IE6下发生Internet Explorer cannot open the Internet site错误
Jun 21 Javascript
Jquery事件的连接使用示例
Jun 18 Javascript
JavaScript定时器详解及实例
Aug 01 Javascript
深入学习JavaScript中的原型prototype
Aug 13 Javascript
jquery 仿锚点跳转到页面指定位置的实例
Feb 14 Javascript
vue 数组和对象不能直接赋值情况和解决方法(推荐)
Oct 25 Javascript
React Native中导航组件react-navigation跨tab路由处理详解
Oct 31 Javascript
Servlet3.0与纯javascript通过Ajax交互的实例详解
Mar 18 Javascript
vue结合element-ui使用示例
Jan 24 Javascript
vue-router两种模式区别及使用注意事项详解
Aug 01 Javascript
解决elementUI 切换tab后 el_table 固定列下方多了一条线问题
Jul 19 Javascript
充分发挥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
php之XML转数组函数的详解
2013/06/07 PHP
Thinkphp使用mongodb数据库实现多条件查询方法
2014/06/26 PHP
php压缩和解压缩字符串的方法
2015/03/14 PHP
基于Jquery的简单&简陋Tabs插件代码
2010/02/09 Javascript
不要在cookie中使用特殊字符的原因分析
2010/07/13 Javascript
JavaScript面向对象编程入门教程
2014/04/16 Javascript
使用jquery.qrcode生成彩色二维码实例
2014/08/08 Javascript
JavaScript判断浏览器类型的方法
2015/02/10 Javascript
jquery实现焦点图片随机切换效果的方法
2015/03/12 Javascript
在AngularJS应用中实现一些动画效果的代码
2015/06/18 Javascript
深入理解JavaScript单体内置对象
2016/06/06 Javascript
深入学习jQuery中的data()
2016/12/22 Javascript
JS实现JSON.stringify的实例代码讲解
2017/02/07 Javascript
jQuery插件FusionCharts绘制2D柱状图和折线图的组合图效果示例【附demo源码】
2017/04/10 jQuery
jQuery插件FusionCharts绘制的2D双柱状图效果示例【附demo源码】
2017/05/13 jQuery
Vue.js中对css的操作(修改)具体方式详解
2018/10/30 Javascript
js实现移动端吸顶效果
2020/01/08 Javascript
echarts实现获取datazoom的起始值(包括x轴和y轴)
2020/07/20 Javascript
JavaScript大数相加相乘的实现方法实例
2020/10/18 Javascript
Python中用format函数格式化字符串的用法
2015/04/08 Python
整理Python中的赋值运算符
2015/05/13 Python
Python 快速实现CLI 应用程序的脚手架
2017/12/05 Python
使用实现XlsxWriter创建Excel文件并编辑
2018/05/04 Python
解决python爬虫中有中文的url问题
2018/05/11 Python
python实现顺序表的简单代码
2018/09/28 Python
Flask框架模板渲染操作简单示例
2019/07/31 Python
python打开文件的方式有哪些
2020/06/29 Python
基于Python编写一个计算器程序,实现简单的加减乘除和取余二元运算
2020/08/05 Python
pytorch学习教程之自定义数据集
2020/11/10 Python
scrapy在python爬虫中搭建出错的解决方法
2020/11/22 Python
医科大学生毕业的自我评价分享
2013/11/12 职场文书
缅怀先烈演讲稿
2014/09/03 职场文书
2014年党务公开工作总结
2014/12/09 职场文书
《伯牙绝弦》教学反思
2016/02/16 职场文书
Python制作春联的示例代码
2022/01/22 Python
Java中Quartz高可用定时任务快速入门
2022/04/03 Java/Android