使人“头大”的 Nodejs 内存泄露

线上环境竟然遇到了内存泄露,通过 3 天的摸索,算是解决了:javascript

更换 pm2 版本,由 v3.5.1 降为 v3.4.1

对于这个结论仍是不太满意,算是歪打正着。不过仍是记录下这几天积累的经验,说不定对正在看此文的你会有帮助。html

前言(鼓励)

有幸,我此次有充足的时间去排查,没有其余事情干扰。但线上出现内存泄漏,解决起来比业务代码 bug 更难解决,那种头皮发麻的难受。缘由以下:前端

  • 项目代码复杂。排查“泄漏点”,犹如大海捞针
  • 开放的前端模式。顾及不暇的 node_modules 模块
  • 无形的压力。须要时间拿数据作佐证,时间等的越长,压力越来

无论有没有精力 or 能力,我认为 解决内存泄漏的最佳实践是:积极的心态 + 冷静的问题定位。(没错,这是对目前没有解决问题的你说的)java

内存增加的缘由

能够看下这篇文章 Finding And Fixing Node.js Memory Leaks: A Practical Guidenode

  • 全局变量
  • 代码缓存
  • 闭包
  • ...

Anyway,其实无论什么缘由,主要仍是 各类引用 得不到 V8 GC 的释放。扔一段很经典的代码:python

function calc(data) {
  return Math.round((data / 1024 / 1024) * 100) / 100 + " MB";
}
function logger() {
  let mem = process.memoryUsage();
  console.log(new Date(), "memory now:", calc(mem.rss));
}
var theThing = null;
var replaceThing = function() {
  logger();
  var originalThing = theThing;
  var unused = function foo() {
    if (originalThing) {
      console.log("未被调用,但 originalThing 有个 someMethod 的引用");
    }
  };
  theThing = {
    longStr: new Array(1000000).join("*"),
    someMethod: function() {
      console.log("没作任何事情,但我是闭包");
    }
  };
  console.log("parse");
};
setInterval(replaceThing, 10);

执行没多久,就从 20M 飚到 几百 M。究其缘由,仍是由于闭包引用没有及时被销毁。linux

具体缘由以下:c++

虽然 unused 没有被调用,可是其中包含 originalThing 并指向 theThing ,theThing 在定义时有个 someMethod 方法,其就是个闭包(能够访问到 originalThing) ,该闭包在 unused 中因为引用了 originalThing 一直没有被释放。git

层层嵌套的引用

对内存进行“检查”

若是你对这块有查询过相关资料, heapdump 这模块应该频繁出现,这里说下如何使用它来监控项目的内存状况。固然还有 memwatch ...github

安装

npm install heapdump -S

若是你足够幸运,确定会出现以下问题:

error: #error This version of node/NAN/v8 requires a C++11 compiler

原版本太低,须要更新 linux 系统的 gcc 等相关库。

参考以下安装说明:

# 安装 repo 仓库
wget http://people.centos.org/tru/devtools-2/devtools-2.repo
mv devtools-2.repo /etc/yum.repos.d

# 安装新库
yum install devtoolset-2-gcc devtoolset-2-binutils devtoolset-2-gcc-c++

# 备份原库
mv /usr/bin/gcc /usr/bin/gcc-4.4.7
mv /usr/bin/g++ /usr/bin/g++-4.4.7
mv /usr/bin/c++ /usr/bin/c++-4.4.7

# 建立快捷方式,将新库链到须要目录
ln -s /opt/rh/devtoolset-2/root/usr/bin/gcc /usr/bin/gcc
ln -s /opt/rh/devtoolset-2/root/usr/bin/c++ /usr/bin/c++
ln -s /opt/rh/devtoolset-2/root/usr/bin/g++ /usr/bin/g++

# 检查版本,肯定生效
gcc --version

固然系统是 window 可能还会有更坑的问题:安装 node-gyp、python 出错。

推荐以下 npm 模块:

windows-build-tools

一键安装相关组件依赖(咱们只要静静的等待,由于时间有些久)。他会帮你安装 window 对应的 NET Framework,python 这些插件。

npm install --global --production windows-build-tools

使用

线上很简单的作了一个控制台输出,用于定位问题:

var heapdump = require("heapdump");
let startMem = process.memoryUsage();

function calc(data) {
  return Math.round((data / 1024 / 1024) * 10000) / 10000 + " MB";
}
// 使用的是 koa
router.all("/foo", async (ctx, next) => {
  let mem = process.memoryUsage();
  logger.debug("memory before", calc(startMem.rss), "memory now:", calc(mem.rss), "diff increase", calc(mem.rss - startMem.rss));
  // ...
});

这样就能实时看到系统的内存消耗(对比刚启动时):

2019-09-06 16:30 +08:00: [2019-09-06T16:30:06.700] [DEBUG] transfer - memory before 55.5898 MB memory now: 95.1484 MB diff increase 39.5586 MB
2019-09-06 16:30 +08:00: [2019-09-06T16:30:07.724] [DEBUG] transfer - memory before 56.2148 MB memory now: 69.8438 MB diff increase 13.6289 MB
2019-09-06 16:30 +08:00: [2019-09-06T16:30:10.406] [DEBUG] transfer - memory before 56.2148 MB memory now: 70.5977 MB diff increase 14.3828 MB
2019-09-06 16:30 +08:00: [2019-09-06T16:30:11.018] [DEBUG] transfer - memory before 55.5898 MB memory now: 95.4219 MB diff increase 39.832 MB
2019-09-06 16:30 +08:00: [2019-09-06T16:30:12.827] [DEBUG] transfer - memory before 55.5898 MB memory now: 95.6797 MB diff increase 40.0898 MB
2019-09-06 16:30 +08:00: [2019-09-06T16:30:12.952] [DEBUG] transfer - memory before 55.5898 MB memory now: 94.9688 MB diff increase 39.3789 MB

添加个快照路由,在按照须要抓取内存此刻使用状况:

router.all("/snapshot", async (ctx, next) => {
  heapdump.writeSnapshot("./dump-" + Date.now() + ".heapsnapshot", function(err) {
    if (err) console.error(err);
  });
});

导入到 chrome 的 profile 面板中,对比先后两个文件的变化,定位问题

图片描述

一切顺利就能很快定位到问题代码。但实际要更困难,更摸不着头脑。

猜想的可能点

若是按照上述的“检查”操做仍是没有定位到问题点,可能这段会对你有所帮助。

首先要知道本身着手的项目的用途,所用技术,它对你排查问题更有指向意义。

好比:此项目是基于 node 的中间层服务,对 api 接口进行转换。用于由后端 api 服务的“升级”平滑各客户端的发版时差。(服务可能在 api 调用上有性能瓶颈?)

技术栈:sequelize + koa + pm2 (熟悉项目的主要框架,从大技术方向着手)

有幸有个 项目 B 和此项目相似,技术上略有差别。

综上所述,就猜想了几个可能的 内存泄露 缘由(附参考文章):

验证

可能缘由 测试方式 验证结果 备注
代码问题 ab 压测 ok 但需增长重视
sequlize 版本问题 ab 压测 ok 暂不尝试。目前使用 4.42.0,线上没法承担更换版本的风险
Ladash _.template ab 压测 ok 保持现状
log4js 有背压问题 ab 压测 ok pm2 & log4js 使用不够友好,在有替代方案前(好比 winston),保持现状
pm2 版本问题 线上下降版本 3.5.1 -> 3.4.1 待验证 项目 B 使用 3.4.1

结论

这两天从线上状况上看,修改 pm2 版本后,内存泄漏获得控制。下面由此结论反推验证步骤:

  1. 使用 devtools ,发现一天的内存差别,TIMERWRAP 指标突增:

    profile

    TIMERWRAP 是 Node 里 Timer 相关定义,猜想是否有定时器在有规律的无限刷新占用性能。

  2. 继续查看,发现 pm2 有些“格格不入”

    profile

    metrics 心跳检测,是否有 setTimeout 之类的代码?

  3. 查看资料,发现相关 issus 中说代码有 leak

    @pm2/io

    查看线上项目相关代码,的确有这段问题代码:

    @pm2/io

    对于为什么这段 if/else 会形成内存泄漏,有空再研究下。估计相似 dom 的 event 绑定没有解除所致。

参考

我只是知识点的“加工者”, 更多内容请查阅原文连接 , 同时感谢原做者的付出:

关于我

若是你以为这篇文章对你有帮助, 请点个或者分享给更多的道友。

也能够扫码关注个人 微信订阅号 - [ 前端雨爸 ], 第一时间收到技术文章 , 工做之余我会持续输出

bVbtL5R?w=258&h=258

最后感谢阅读, 大家的支持是我写做的最大动力

相关文章
相关标签/搜索