浅析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 相关文章推荐
JavaScript在IE和Firefox浏览器下的7个差异兼容写法小结
Jun 18 Javascript
struts2+jquery+json实现异步加载数据(自写)
Jun 24 Javascript
一个非常全面的javascript URL解析函数和分段URL解析方法
Apr 12 Javascript
javascript实现瀑布流自适应遇到的问题及解决方案
Jan 28 Javascript
实例详解jQuery表单验证插件validate
Jan 18 Javascript
利用js+css+html实现固定table的列头不动
Dec 08 Javascript
用jQuery.ajaxSetup实现对请求和响应数据的过滤
Dec 20 Javascript
nginx配置React静态页面的方法教程
Nov 03 Javascript
使用vue-aplayer插件时出现的问题的解决
Mar 02 Javascript
vue计算属性及使用详解
Apr 02 Javascript
vue微信分享的实现(在当前页面分享其他页面)
Apr 16 Javascript
react使用antd的上传组件实现文件表单一起提交功能(完整代码)
Jun 29 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 include_path设置技巧分享
2011/07/03 PHP
php验证session无效的解决方法
2014/11/04 PHP
PHP 配置后台登录以及模板引入
2017/01/24 PHP
Yii2 hasOne(), hasMany() 实现三表关联的方法(两种)
2017/02/15 PHP
JavaScript XML实现两级级联下拉列表
2008/11/10 Javascript
JQuery 入门实例1
2009/06/25 Javascript
JavaScript建立一个语法高亮输入框实现思路
2013/02/26 Javascript
javascript实现跳转菜单的具体方法
2013/07/05 Javascript
jQuery Validate插件实现表单强大的验证功能
2015/12/18 Javascript
jQuery短信验证倒计时功能实现方法详解
2016/05/25 Javascript
javascript实现下雨效果
2017/03/27 Javascript
JS实现线性表的链式表示方法示例【经典数据结构】
2017/04/11 Javascript
Vue.js学习笔记之修饰符详解
2017/07/25 Javascript
原生JS实现的跳一跳小游戏完整实例
2019/01/27 Javascript
javascript实现小型区块链功能
2019/04/03 Javascript
bootstrap-table formatter 使用vue组件的方法
2019/05/09 Javascript
js+canvas绘制图形验证码
2020/09/21 Javascript
python获得linux下所有挂载点(mount points)的方法
2015/04/29 Python
浅谈Python_Openpyxl使用(最全总结)
2019/09/05 Python
基于Python实现2种反转链表方法代码实例
2020/07/06 Python
Python实现疫情地图可视化
2021/02/05 Python
ASP.NET中的身份验证有那些
2012/07/13 面试题
介绍一下XMLHttpRequest对象
2012/02/12 面试题
商务英语本科生的自我评价分享
2013/11/15 职场文书
大学生职业生涯规划书汇总
2014/03/20 职场文书
《从现在开始》教学反思
2014/04/15 职场文书
工商局领导班子存在的问题整改措施思想汇报
2014/10/05 职场文书
处级干部考察材料
2014/12/24 职场文书
初中教师个人工作总结
2015/02/10 职场文书
成品仓库管理员岗位职责
2015/04/09 职场文书
2015领导干部廉洁自律工作总结
2015/07/23 职场文书
关于销售人员的年终工作总结要点
2019/08/15 职场文书
7个你应该知道的JS原生错误类型
2021/04/29 Javascript
详解Java线程池是如何重复利用空闲线程的
2021/06/26 Java/Android
海贼王十大逆天果实 魂魂果实上榜,岩浆果实攻击力最强
2022/03/18 日漫
SQL中的连接查询详解
2022/06/21 SQL Server