看我如何搞定Nodejs内存泄漏问题

最近又用node写了一个小工具,需要常驻进程,经过几天的观察,发现内存占用有持续增加的趋势(虽然不明显,但还是让我察觉到了,我真屌)。突然发现,
我竟然不知道怎么排查nodejs的内存泄漏,吓死宝宝了!

花时间看了一下相关资料(google真好,外果仁真屌),看来这部分也已经有比较完善的方法论+工具了。所以这篇文章记录一下自己从不懂到入门的经历~~
我希望这篇文章不仅能提供具体的工具供大家使用,还提供足够的理论知识来辅助大家思考,当然,也可能是我自己想多了~~哇哈

发现问题

由于没有太多运维经验,也不知道啥逆天的工具来帮我一键式监控所需要的指标,如果你和我情况一样,那我们只能手动来造个简陋的但够用的监控脚本了。

别告诉我你和我一样shell也不熟,直接就node吧。少废话~

先装上pm2,然后写一个脚本,来定时打印目标应用的内存使用率,当然,前提是目标应用也都放在pm2中管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const exec = require('child_process').exec;
var Later = require('later');
var schedule = Later.parse.text('every 5 mins'); // 每5分钟正点触发
Later.setInterval(function(){
exec('pm2 jlist', { // 打印出pm2中应用的基本状态信息,输出是json字符串
timeout: 2000
}, (err, data, stderr)=>{
if (err) {
console.error(err, err.stack); // an error occurred
return;
}
//将结果写入日志
data = JSON.parse(data);
if(data[0]){ // 这里取0是因为我希望监控的应用在pm2中的顺序是第一位
console.log(data[0].monit.memory/(1024*1024)); // 直接输出到pm2的log中
}
});
}, schedule);

然后就等一段时间,就会在对应的log文件中拿到相关的内存数据,然后只需要用电子表格生成一个图标即可,我推荐使用google drive的spreadsheet:

上面的图是我收集了大概2天的内存数据绘制成的图标,可以看出内存使用量成上升趋势。没错,就是泄漏了!!

友情提醒: 修复内存泄漏可能会耗时很久,你最好先找一个临时方案来维持生计,例如定期重启程序。

搭建环境

本着实战为主的策略,我们先从搭建内存泄漏监控环境开始。刚开始参考node-memory-leak-tutorial,以为会很顺利搭建好的,不过碰到了这个error。看Issus应该是个很常见的错误,按照别人的解决方案,尝试切换成nodejs 6.3.1版本进行了测试,确实可以绕过那个错误:

1
2
// 在项目目录下
node-debug leak.js

然后终端会启动你的chrome,并停在代码的断点位置,深吸一口气你就可以点击执行了。

备注:若遇到无法创建快照问题,需要多刷新几次哟~

其它工具我也顺便试了一下:

  1. node-memwatch
  2. node-webkit-agent
  3. node-heapdump

因为它们都需要根据操作系统进行编译,我的本地环境是 win7 64bit,这并不是一个理想的nodejs环境,至少我这么认为,否则也不会碰到恶心的“.net framework”问题。我劝大家千万别学我轻易的就删除了 .net framework 3.5 这个安装包,因为这是win7自带的,删了以后就装不上了,而装更新的4.0+版本的话我这边很重要的一个软件就无法运行了(翻墙你懂的)。在Windows 7系统上安装.NET Framework 3.5框架很不容易的!建议可以用上docker来搭建一个专门用来分析用的容器,这里我就不折腾下去了,its your turn~~

nodejs内存分析的理论姿势

在开始听我正儿八经胡说八道之前,推荐你先看几个文档:

一次性看完这些,可能要花很久,如此贴心的我已经帮你看过了,根据我的理解,总结如下:

  • javascript的v8内存管理和java jvm类似,都有新生代(To-Space and From-Space),老年代等;
  • 排查内存泄漏需要分析内存快照,可以使用已有的工具以devtool的profile面板或代码的方式创建snapshot;
  • 创建的快照文件可以导入devtool的profile进行分析;
  • 快照生成的最佳实践是:先保证程序已经预热,然后进行快照1(先触发GC),然后对程序进行一些交互(例如:对于web服务即http请求),再次创建快照2,如此循环来生成多个版本的快照;
  • 合理的利用devtool的profile提供的功能,正确的选择视图;
  • 理解profie中的字段含义:
    • 对象上的黄色标识表示的是javascript直接引用,红色表示间接依赖引用,不太需要关注的是无底色对象,其代表被其它资源引用(如:natvie code);
    • profile会根据对象的构造方法对对象进行分组归类,每个组对应的“Shallow Size”表示的是该组对象的直接内存占用大小(例如:该类对象自身的原始类型数据的内存占用),对应的“Retained Size”表示的是该组对象依赖的其它对象而造成的内存占用总数(等于自身的Shallow Size + 依赖对象的Shallow Size [ + 依赖对象的依赖对象的Shallow Size [ + 递归下去]]);
    • 由于性能原因,profile中不会显示对象的整型类型的属性,但是它们并没有丢失,仅仅是工具没有显示出来而已。
  • 应该警惕“distance”比较大或比较小的对象,总之和其它同类型对象的distance不一样就意味着可能有问题;
  • 尽量不要用匿名函数,函数有名字会让分析更容易,其实更推荐的是使用OOP,这样会最容易定位需要追踪的变量,毕竟都是构造器创建出来的嘛;
  • 闭包(匿名函数,定时器等)创建的上下文引用很容易造成不易察觉的内存泄漏;
  • console的相关函数(log, error等)在实际分析中发现其引用的变量无法释放,可以参考#1741,所以你可以在测试代码中替换掉console的相关函数(这样你就不需要改动被测代码逻辑了);
  • 对象上的事件监听器的闭包最容易造成泄漏,即便是使用once,也可能一次都没有触发而导致该回调函数无限期引用数据。

ok,一大堆姿势足够你花很久时间阅读了。不过并不是说你看了这些内容,就可以轻松战胜困难了。还有一个环节我们没有讨论:若你的项目足够复杂(大),那要怎么搭建项目的测试环境呢?

这里我认为,大概需要按照下面的步骤来做:

  1. 将完整的项目拆解成独立的不同块,并为每个拆解后的小模块写测试代码
  2. 针对定时器相关的逻辑,最好改成手动触发,或利用测试库(sinonjs)模拟时间片段
  3. 初期可以先尽可能排除依赖的第三方库,最后酌情去测试它们(如果你怀疑是它们的问题的话)
  4. 低级别异常伪造(例如socket,file等)要靠伪造对应方法(不推荐使用sinonjs.stubs,因为它保存每次调用时的参数数据,影响你观察,不妨试试mockery
  5. 最终还是有必要放在线上环境实测一段时间来观测问题是否真的修复了

我们主要来说一下第5条,其意味着你要在线上环境想办法导出快照到本地来分析。下面来看看怎么做:

首先,你给线上环境中安装v8-profiler库,它用来提供创建快照的功能。

然后,看一下下面的这段样板代码,其意义在于在你的项目中加载v8-profiler库,并提供一个对外指令用来通知它创建快照文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var fs = require('fs');
var profiler = require('v8-profiler');
// ---------------
// 测试目标
function LeakingClass(){}
var leaks = [];
setInterval(function(){
for(var i = 0; i < 100; i++){
leaks.push(new LeakingClass);
}
console.error('Leaks: %d', leaks.length);
}, 1000);
// ---------------
// 指令服务
var koa = require('koa');
var route = require('koa-route');
var service = koa();
var snapshotNum = 1; // 用于为生成的快照进行编号
service.use(route.get('/snapshot', function *(){
var response = this;
var snapshot = profiler.takeSnapshot();
snapshot.export(function(error, result) {
fs.writeFileSync((snapshotNum++) + '.heapsnapshot', result);
snapshot.delete();
response.body = 'done';
});
}));
service.listen(2333, '127.0.0.1'); // 推荐绑定内网ip,不要允许外网访问该服务

每次请求http://127.0.0.1:2333/snapshot,你都会在项目根目录生成一个快照文件,然后把它下载到本地磁盘就可以在chrome里随时进行分析了。

总结

在实际排查过程中,发现最难测试的还是依赖的第三方库的泄漏问题。毕竟你无法理解它们的实现。但不可能所有逻辑都自己来完成,所以面对各种各样的第三方类库,还是建议选择尽可能权威的,主流的。剩下那些很小的功能模块,就只能花时间研读其实现代码了。

如果你的业务采用了生产消费者模式,你的测试脚本一定要保证任务的生产和消费的速率保持同速率(或者干脆确保消费者处理完一批次的任务耗时一定要小于批次创建的间隔时间),不然由于任务得不到处理,必然会产生任务积累,看起来就好像有内存泄漏一样,但其实这种情况其实是合理的,只是说明你的消费者太少了而已。

另外,一定要最大频度的,尽可能长时间的运行测试代码,才能明显的暴露出问题,例如:

1
2
3
setInterval(function memoryleakBlock(){
// 待测试的代码块
}, 100);

注意在上面memoryleakBlock中避免引用全局变量哟,这样你运行一夜,第二天上班来看结果(如果它还跑着的话)。

Request使用proxy导致的socket连接数泄漏问题

这是一篇快餐文章,源于前天接到了服务器预警邮件,提示:连接数超过阀值。

快速定位问题,发现是因为之前跑的一个nodejs开发的小程序,该程序用于定时去指定网站采集相关数据的。

由于要采集的网站需要设置翻墙代理,很麻烦的。之前直接使用Request来发http请求也没碰到这个问题。所以归结为是由于使用proxy不稳定,导致大量的socket异常。

在进入主题之前,先吐槽一下Request这个库,它的文档看似详实,但如果你仔细阅读就会发现很简陋。很多地方感觉欲言又止,很是让人烦躁。

尝试去看它的源码,发现它返回异常的方式采用了2种不同的方法(一般异步方法都如此处理):

  • 沿用nodejs的回调返回异常风格
  • 发布异常事件

这也是一开始总是被无法捕获的异常导致程序crash的原因。

去github上的issues翻到别人的异常捕获异常方法,也是醉了:

1
2
3
4
5
6
7
Request
.get({/*some config*/}, function(err, response){/*回调逻辑,第一个参数为error*/})
.on('error', function(error){
// 再次绑定error事件
console.error('Request on error');
console.error(error.stack);
});

按道理说,这两种方式同时只需要用一种,同时使用两种则会发生重复调用问题。但如果我们没有绑定error事件(只采用回调风格),socket级别的异常是无法捕获得到的,这也就导致了上面的方案。不过经过绑定了这个error事件后,发现几乎Request触发的所有异常都被其捕获到了,不会再因为无法捕获的异常而导致程序crash了(还存在一些异常,我建议process.on('uncaughtException')还是不能省)。

接下来主题,先看一下一个issue,题主已经详细描述了问题也给了解决方案。
实测了一天,感觉确实解决了,至少从图标中观测正常许多了:

如果去看源码,会发现很绕,毕竟到处可见的回调和事件流。不过从解决方案上来看,很直观。思路就是在: 确保在链接出现异常后也触发回调,在回调中来处理善后工作,最终会回收相关资源,而不是直接清除掉必要的事件监听器

去看源码吧,你将会收获更多。

去年的今天就是你的Deadline

我最近有个感悟:时间,最复杂的存在。

像我这样一个从没有跨过时区的孩子,是感受不到时间算法的复杂度的。直到有一天,我们的系统需要面对多时区的场景,我才恍然大悟。简简单单的一个时区问题都已经让系统很多基于时间的逻辑变得复杂,更不要提分布式时序问题了(我们今天并不讨论这个问题,我也讲不好)。

阅读全文

Javascript项目单元测试小结

这几天在写一个采集程序,用nodejs。老实讲这是第二次(相对正式)写和采集有关的模块,第一次是因为要定时访问自身项目来触发缓存的,细节可以看这里。后来从上一家公司离职,也没有继续维护这个程序了。
这次算是正宗的采集模块(确实是从目标网站中获取有价值的内容,我就不方便透露具体细节了~),而且这个项目是正式产品,不像之前公司那样扯~~这篇文章本身不是讲采集程序细节的,其实个人感觉开源的采集系统已经很成熟了,如果不是有特别特殊的需求,还是不要重复造轮子的好。

阅读全文

译-Sinon入门:利用Mocks,Spies和Stubs完成javascript测试

最近写完一个基于nodejs的组件后的我打算为其写一下单元测试,本以为之前了解过相当多的关于测试的知识,应该可以很顺利的搞定,可真的去写测试项的时候才发现依然存在一些需要克服的困难。不过,接下来翻译的这篇文章就是专门针对测试神器的,应该可以帮到和我一样的新手。

原文地址:https://www.sitepoint.com/sinon-tutorial-javascript-testing-mocks-spies-stubs/

阅读全文

一次被咨询后的感触

上周末陪着朋友去谈项目,刚好派单方是我以前的同事(好吧,其实我就是传说中的介绍人~)。一进办公室,对方的程序员就开始直奔主题,让我感到些许的不舒服。别误会,我十分喜欢开门见山直奔主题,但项目背景肯定是不能省的,直接进入具体功能细节真的让我感觉不适应。不过考虑到我只是个旁听,所以也就耐着性子听了一会~不一会老板来了,我就被拉出来私聊了。

阅读全文

十月份杂谈

Long time no write.

整整一个多月没有更新blog,原因有很多,但主要还是忙~

现在工作岗位偏重需求分析,我需要花大量的精力在与需求方一起讨论工作流程和系统的落地方案上。纯粹的技术性质的问题一般都会交给
其余同事负责跟进。所以一时不知道应该写什么在博客,毕竟之前博客都是偏向具体技术问题的主题。

阅读全文

如何基于接口文档生成模拟数据

前后端分离是目前主流的团队开发方式,有效的隔离不同技术领域开发人员的依赖,包括开发进度上的依赖和代码文件上的依赖。而做到这一点,全靠一个规则:依赖接口而不依赖实现。

那么,web领域前后端分离,通常都会选择基于http+json的方式,也就是大家说的restful类型的接口。在进入代码编写之前,团队会定义好所有依赖的rest接口,前后端开发人员遵照接口文档各自完成自己的工作,完美!

阅读全文

译-在多个标签页之间共享sessionStorage

原文:Sharing sessionStorage between tabs for secure multi-tab authentication

译者得er瑟


昨天,就在昨天,前端一同事提了一个问题:我们的系统,用户重新开一个标签页,就要重新登录。我当时觉得这怎么可能?结果现场一测,还真是,好尴尬!

今天抽了点时间网上查了查,才发现原来一直以为很简单的sessionStorage,还真埋了这么一颗雷。不过国外前辈也提出了一个解决方案,不仅如此,文章还把浏览器端保存数据的场景分析的很透彻,所以斗胆翻译了一下。

阅读全文

小团队玩不转的测试

早在上一家公司,就为测试问题头疼过,那时候测试全靠人肉,还整出了黑盒白盒测试文档,还要对代码进行打点,还要人工去匹配打点数据是否执行…都是泪,都是泪,都是泪啊!

那个时候团队人数最多时有十二个,项目现在想想也不算大,按道理是可以分出来一部分人来专做测试工作的,只是当时无法说服领导成立测试小组,毕竟自己也没有自动化测试的经验。最后强迫别的部门的同事来帮我们测,除了心不甘请不愿外,测试的结果也不是特别的理想。

换了一家公司,依然是四人小团队,现在我开始琢磨如何自动化测试了。毕竟之前的不愉快精力,再加上我现在的岗位更多是解决团队的开发效率问题,所以必须得正视这个头疼的问题。

阅读全文

Git Hook帮你维护前端代码规范

只要不是一个人在战斗,你都一定会碰到很多工程问题。我们今天来说的,就是代码格式问题。这不是个什么有意思的话题,这个话题讲的就是条条框框,就是枯燥,就是没意思。但是,如果你的团队缺少代码格式规范的话,当你review组员的代码时,你就会感觉在吃屎,没错,不夸张!

我不怀疑团队组员的积极性,因为条条框框本来就不是程序员的调调,而且人类和机器的最大差别就是遵守规范的程度。所以,你不能要求你口头上说代码格式要怎样怎么,所有同事就会立刻写出符合要求的完美代码。

阅读全文

Open-Falcon初探

随着项目的开发一点一点的完成,离初版上线日期已经越来越近了,这样就涉及到各种运维问题,监控的意义就体现出来了。虽然项目最终会部署在云平台,而云平台自身会带监控套件,不过不够灵活,一些想要的指标和报警方式还是需要自己来实现。大概看了几款监控解决方案,对Open-Falcon特别有好感,虽然我不懂GO语言~

阅读全文

传统Web项目代码变更引起的浏览器缓存问题和解决思路

不确定文章的标题描述的是否已经足够明了,至少用类似的描述在gg中并没有定位到相关的文章。讨论这个话题的更多是围绕这前端工程化套件的用法的(例如webpack、grunt、gulp等),而这些工具对单入口页面SPA应用支持度非常的好。但现实往往不尽如意,面对传统的多入口web项目,前端又应该如何解决浏览器缓存旧版本代码的问题呢?

阅读全文