Node内存泄漏 node-cron和pm2共同使用造成内存泄漏

        前不久在自己的Node服务器上线了一个新功能后,今天打开pm2 list一看,node服务器的mem使用竟然达到了500M+,在上线新功能之前,这个Node服务器的内存使用一直在70-80M上下,猜想应该是内存泄露了,于是便有了这次内存泄漏排查。

        首先重启了一下服务器,可看到内存持续上涨

一.误入歧途:是PM2造成的吗?

 由于之前服务器采用pm2守护启动,第一个想到的排查方向:

        使用正常的本地node 启动服务器,并查看内存快照。若使用node启动没问题,而使用pm2守护启动造成内存泄漏,则猜测有可能是pm2的daemon等或是其他pm2配置造成的

        于是将最新版本node拷贝到本地,并安装easy-monitor,对本地非pm2启动的服务器进行内存监控。

 监控结果如下:

 通过分析发现:

        在本地node index.js启动的服务器内存占用在20-30M之间,并没有出现部署时500M+的情况,于是我便在PM2仓库下检索与daemon相关的ISSUE,通过测试禁用daemon等方法后,❌仍然是非PM2启动不爆内存,但只要使用PM2启动内存就爆到500M+


二.罪魁祸首 node-cron

        在上一个方向排查无果后,我想到了另一个可能出现问题的地方。由于在本次上线的新功能中,导入了一个之前未使用过的node-cron

        本次上线的功能有如下需求:在每天指定时间段发出指定网络请求完成一些事情。

而node-cron是一个可以在node中进行任务调度的包。

调度的例子如下:


const cron = require("node-cron");
const successTask = cron.schedule(
  "55 59 19 * * *",
  () => {
    // 注册预约轮询器
    reserveInterval = setInterval(() => {
      reserveSeat();
    }, 900);
  }
  {
    timezone: "Asia/Shanghai",
  }
);
successTask.start()

         抱着试一试的态度,当我将新上线的版本回滚到未使用node-cron的旧版本再使用pm2启动一段时间后,发现此时服务器不爆内存了,至此,可以初步判断造成内存泄露的罪魁祸首就是与node-cron相关的代码。

        在node-cron仓库的ISSUE区进行memory leak相关的搜索后,果不其然找到了类似的ISSUE,可以看到这个ISSUE已经Open很长时间了:

Excessive CPU/RAM usage · Issue #358 · node-cron/node-cronicon-default.png?t=N5K3https://github.com/node-cron/node-cron/issues/358

 值得注意的是,在评论区有人指出:若在初始化调度任务时,使用了timezone配置,将导致内存泄露,删除timezone配置后内存泄漏消失

        而我的 多个调度任务恰巧均使用了timezone,于是我将几个任务的timezone配置干掉🪓,再次使用pm2启动服务器
// 干掉timezone
const successTask = cron.schedule(
  "55 59 19 * * *",
  () => {
    // 注册预约轮询器
    reserveInterval = setInterval(() => {
      reserveSeat();
    }, 900);
  }
  // {
  //   timezone: "Asia/Shanghai",
  // }
);

        果不其然,此时服务器也没有爆内存。现在可以确定的是:

就是多个使用了timezone配置的调度任务 + 使用pm2启动时共同造成了内存泄露。


三.原因分析

知道了问题所在,我们不妨来分析造成此次内存泄漏的原因

        1.node-cron源码timezone分析

        可以看到,源码实现时,只要调度任务存在timezone配置,就会新建一Intl.DateTimeFormat实例,当创建的实例【过多】时就可能会导致内存泄露。

        2.结合PM2分析

        如何定义上述提到的【过多】是个有趣的问题。根据之前的测试可知,我们在本地不使用pm2启动node服务器时:

        即便多个调度任务均配置了timezone,但easy-monitor仍显示内存正常(20~30M)。若换用pm2则会出现问题,我猜想是由于我使用的PM2是Fork模式造成的。

         由于平常node服务器也只是跑跑自己的一些小应用,就一直使用PM2默认的fork模式,没注意其实现原理。

        这次再启动PM2应用时,我们不妨用htop监控一下PM2,如下图所示:

可看到确实fork了多份

         且(干掉timezone后的服务器)占用的mem稳定在90M左右,比easy-monitor监控出(20M左右)高出四五倍,但不会像之前一样爆到500M+

         我猜想应该就是在PM2启动时的fork阶段复制多份子进程时,new了多份Intl.DateTimeFormat实例,且份数一定比直接使用node index.js启动时new的多。

在这个过程中,达到了所谓的【过多】界限,从而导致内存泄露。


四. 改过自新

        内存泄漏的原因算是找到了,但新功能还是要上线的🤡,有没有什么解决办法呢?

我在翻阅ISSUE区后,找到了两种可以修复该内存泄漏的方法:

1. ✅【推荐】node-cron再见,croner你好

        croner也是一个在node端提供任务调度的包,亲测其不会造成内存泄漏,以下给出一个修改调度任务的示例:

参考链接Use croner instead of node-cron · rjmccluskey/wordle-leaderboard-discord-bot@830da55 (github.com)icon-default.png?t=N5K3https://github.com/rjmccluskey/wordle-leaderboard-discord-bot/commit/830da555e95c83081a3c8bb4c76b9144a2cf3f91        值得注意的是,使用croner初始化调度任务时,不需要再像node-cron一样初始化后调用start方法注册任务,croner的调度任务声明完即注册。

//const cron = require("node-cron");
const { Cron } = require("croner");

/**
 * @deprecated 内存泄露弃用
 */
// // 循环预约请求
// const successTask = cron.schedule(
//   "55 59 19 * * *",
//   () => {
//     reserveInterval = setInterval(() => {
//       reserveSeat();
//     }, 900);
//   }
//   // {
//   //   timezone: "Asia/Shanghai",
//   // }
// );
// successTask.start();

// 循环预约请求
const successTask = Cron(
  "55 59 19 * * *",
  {
    timezone: "Asia/Shanghai",
  },
  () => {
    reserveInterval = setInterval(() => {
      reserveSeat();
    }, 900);
  }
);

2. ⭕【不推荐】缝缝补补又三年

        ISSUE区已有开发者修复了这个问题,但repo截至(2023/06/29)仍然没有合并这个PR,可通过移除原有node-cron,并下载别的分支解决问题

参考链接:

fixes #358: creates single instance of date formatter by gokulchandra · Pull Request #366 · node-cron/node-cron · GitHubPotential fix for: #358The current implementation creates multiple instances of the Intl.DateTimeFormat causing excessive memory usage.I've tested the current implementation and can confirm that RAM usage has now stabilized.Related issues:https://bugs.chromium.org/p/v8/issues/detail?id=6528nodejs/node#31914https://github.com/node-cron/node-cron/pull/366        个人不推荐这种方法,后期做包版本管理稍微有些麻烦

npm remove node-cron
npm install 'https://github.com/gokulchandra/node-cron/tree/fixes-memory-leak'

 小结一下:

        第一次做内存泄漏的排查,过程很漫长,特此写一篇博客记录。而且发现泄露的时间是周五下午😅😅😅,属于是梦幻开局了,周末大半天又没了。

        同时也没想到几K star,周下载几万的 的库存在这种问题,而且在社区给出解决方案的情况下还让ISSUE一直open了这么久,劝退。

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值