前不久在自己的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很长时间了:
值得注意的是,在评论区有人指出:若在初始化调度任务时,使用了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)
https://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,并下载别的分支解决问题
参考链接:
npm remove node-cron
npm install 'https://github.com/gokulchandra/node-cron/tree/fixes-memory-leak'
小结一下:
第一次做内存泄漏的排查,过程很漫长,特此写一篇博客记录。而且发现泄露的时间是周五下午😅😅😅,属于是梦幻开局了,周末大半天又没了。
同时也没想到几K star,周下载几万的 的库存在这种问题,而且在社区给出解决方案的情况下还让ISSUE一直open了这么久,劝退。