浅析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 对象介绍
Jan 20 Javascript
深入理解JavaScript系列(12) 变量对象(Variable Object)
Jan 16 Javascript
关于JS控制代码暂停的实现方法分享
Oct 11 Javascript
JavaScript图片轮播代码分享
Jul 31 Javascript
Bootstrap多级导航栏(级联导航)的实现代码
Mar 08 Javascript
Javascript打印局部页面实例
Jun 21 Javascript
JavaScript计算值然后把值嵌入到html中的实现方法
Oct 29 Javascript
简单实现bootstrap选项卡效果
Feb 08 Javascript
微信小程序实现倒计时60s获取验证码
Apr 17 Javascript
vue2.0+vue-dplayer实现hls播放的示例
Mar 02 Javascript
微信公众号开发之微信支付代码记录的实现
Oct 16 Javascript
Electron整合React使用搭建开发环境的步骤详解
Jun 07 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版本号
2006/10/09 PHP
php 数组使用详解 推荐
2011/06/02 PHP
PHP简单字符串过滤方法示例
2016/09/04 PHP
php使用SAE原生Mail类实现各种类型邮件发送的方法
2016/10/10 PHP
PHP ajax+jQuery 实现批量删除功能实例代码小结
2018/12/06 PHP
网站上面有这种切换效果
2006/06/26 Javascript
javascript smipleChart 简单图标类
2011/01/12 Javascript
判断js对象是否拥有某一个属性的js代码
2013/08/16 Javascript
jquery链式操作的正确使用方法
2014/01/06 Javascript
一个php+js实时显示时间问题
2015/10/12 Javascript
深入理解javascript中concat方法
2016/12/12 Javascript
微信小程序 详解页面跳转与返回并回传数据
2017/02/13 Javascript
Bootstrap表单控件学习使用
2017/03/07 Javascript
Vue.js实现一个漂亮、灵活、可复用的提示组件示例
2017/03/17 Javascript
详解webpack 多入口配置
2017/06/16 Javascript
Angular 2父子组件之间共享服务通信的实现
2017/07/04 Javascript
详细分析单线程JS执行问题
2017/11/22 Javascript
nodejs基于WS模块实现WebSocket聊天功能的方法
2018/01/12 NodeJs
基于VuePress 轻量级静态网站生成器的实现方法
2018/04/17 Javascript
使用Mock.js生成前端测试数据
2020/12/13 Javascript
Python爬虫辅助利器PyQuery模块的安装使用攻略
2016/04/24 Python
python中单下划线_的常见用法总结
2018/07/10 Python
python读取图片任意范围区域
2019/01/23 Python
python多线程扫描端口(线程池)
2019/09/04 Python
wxPython窗体拆分布局基础组件
2019/11/19 Python
python3 assert 断言的使用详解 (区别于python2)
2019/11/27 Python
python TCP包注入方式
2020/05/05 Python
聊聊python中的异常嵌套
2020/09/01 Python
PyQt QMainWindow的使用示例
2021/03/24 Python
幼儿园教育教学反思
2014/01/31 职场文书
软件售后服务承诺书
2014/05/21 职场文书
刑事起诉书范文
2015/05/19 职场文书
少年的你:世界上没有如果,要在第一次就勇敢的反抗
2019/11/20 职场文书
JavaScript继承的三种方法实例
2021/05/12 Javascript
MySQL索引 高效获取数据的数据结构
2022/05/02 MySQL
码云(gitee)通过git自动同步到阿里云服务器
2022/12/24 Servers