西西软件园多重安全检测下载网站、值得信赖的软件下载站!
软件
软件
文章
搜索

首页 → 业内动态 → 业内资讯 → Node.js进程内存涨到2G不降:V8堆快照揪出没释放的定时器

Node.js进程内存涨到2G不降:V8堆快照揪出没释放的定时器

相关软件相关文章发表评论 来源:本站整理时间:2026/11/4 2:09:14字体大小:A-A+

作者:xiix点击:0次评论:0次标签: Node.js内存泄漏 V8堆快照 内存排查 heapdump 服务器内存

Node.js x64版v18.14.1.0 官方版
  • 类型:编程辅助大小:29.9M语言:中文 评分:9.6
  • 标签:
立即下载

上个月接了一个外包团队的 Node.js 接口服务,跑在一台 4 核 4G 的云服务器上。功能没什么问题,但运维那边每天早上都要发一条告警:常驻内存超过 1.5G 就重启。重启之后安静几个小时,然后又慢慢往上爬。我去看了下监控曲线,那条线不是尖峰,是一条标准的斜坡,从 400M 一路爬到 2G,用了整整两周。这种曲线只有一种解释进程里的东西在不断累积,而且从来没人去收。

如果你正在排查 Node.js 这类服务的内存占用,或者需要一台内存规格合适的机器先把复现环境搭起来再动代码,可以先看看这两家的配置和技术说明:churros(香港/日本/新加坡 BGP 云服务器) EVOXT(高主频云主机)

这类问题最烦的地方在于,它不报错、不崩溃、接口响应还正常,只是内存慢慢涨。很多团队的做法是加内存或者加个定时重启,本质上是把问题往后推,而不是解决它。这次我花了两天时间,用 V8 的堆快照把它挖出来了,写下来给同样在抓瞎的朋友。

第一步:先分清内存涨在哪儿

Node.js进程内存占用监控大盘

很多人一上来就想找“泄漏代码”,但 Node.js 的内存至少分三块,你连涨的是哪一块都没搞清楚,后面全是瞎猜。直接看 process.memoryUsage() 的四个关键字段:

rss 是进程向操作系统要到的物理内存,heapTotal 是 V8 向系统申请到的堆空间,heapUsed 是堆里真正被对象占用的部分,external 是堆外的 Buffer 和 TypedArray。判断方向有个很粗暴但很好用的办法:

如果 heapUsed 和 rss 一起涨,那问题在 JS 对象层面某个 Map、Array 或者闭包一直挂着不放;如果 heapUsed 基本不动但 rss 一路涨,那就是堆外的问题,通常是 Buffer 没释放或者 native 插件自己踩了坑;如果 external 单独疯涨,那就直接查 Buffer 的分配点。我们这台机器上属于第一种,heapUsed 和 rss 是同步爬升的,所以方向很明确:JS 对象被某个东西一直引用着。

第二步:抓一份堆快照

确认方向之后就是取证。Node.js 内置了 v8.writeHeapSnapshot(),产出一个几百 MB 的 .heapsnapshot 文件,本质上是 JSON,记着当下所有对象的引用关系。命令行里可以直接开一个调试端口,用 Chrome DevTools 的 Memory 面板连上去点 Take heap snapshot,效果是一样的。

但这里有个关键动作,很多人做漏了:要抓两份。第一份在服务刚起来或者泄漏还没发生的时候抓一份存着,等内存涨上去之后再抓第二份,然后把两份都拖进 DevTools 的 Memory 面板,用 Comparison 视图做对比。

对比视图会直接告诉你这期间新增了哪些对象、各占多少。最典型的结论长这样:某个 Timer 类型的对象数量从 3 个变成了 46000 个,占了 1.2G。这时候基本就不用猜了,代码里定时器没清这件事已经写在脸上。

第三步:那个吃掉 1.2G 的定时器

V8堆内存对象快照对比分析

翻代码,定位到的是一个数据同步任务,大致长这样:

function startSync() { setInterval(async () => { const rows = await fetchAll(); cache.set('rows', rows); }, 60 * 1000); }

问题不在 setInterval 本身,而在回调里那个 async 函数。定时器每分钟触发一次,如果某次抓数据慢、或者下游接口超时,这个异步任务就还没跑完,下一个定时器已经排上了。每一个未完成的 async 闭包都会连带它引用的整条数据链一起留在堆里包括那个 rows 数组。跑两周就是两万多次,内存自然就撑起来了。

修法不复杂,但要注意两点。第一,回调里要自己判重,上一次没跑完就直接跳过这次:

let running = false; setInterval(async () => { if (running) return; running = true; try { const rows = await fetchAll(); cache.set('rows', rows); } finally { running = false; } }, 60000);

第二,如果这个任务只是要轮询,更稳妥的做法是用 setTimeout 递归调用自己,跑完再排下一次,从根子上避免堆积。改完之后再观察一周,那条斜坡彻底平了,常驻内存稳定在 380M 左右,比原来还低了一截因为原来那 1.2G 里有一大半是重复抓回来的同一批数据。

内存条与定时器图标组合示意

第四步:还有一张永远长不大的缓存表

定时器只是其中一个坑。堆快照对比里还暴露了第二个:某个模块级的 Map,被当成“最近访问记录”一直往里塞 key,从来没做过淘汰。这个 Map 本身不大,但它持有的每一个 value 都是一份完整的业务对象,加起来又是几百 M。

这类无界缓存是最常见的内存杀手,因为写代码的时候特别顺手:查不到就放进 Map,下次直接命中。问题是没人会记得它需要淘汰。修法是给它加个上限,比如用 lru-cache 这种带容量限制的实现,或者自己维护一个简单的先进先出队列,超过阈值就丢最老的。改完之后,堆快照里 Map 的 size 从 80 万降到稳定的两千。

第五步:几个容易忽略的细节

服务器上排查Node.js内存占用现场

不要指望 --max-old-space-size 治本。 这参数只是告诉 V8 堆涨到多少就该开始更积极地回收垃圾,它不阻止你的对象继续被引用着。把它从 2G 调到 4G,曲线只是从第 5 天爬变成了第 10 天爬。真正需要它的地方是防止进程被内核 OOM 直接杀掉,属于兜底而不是解法。

事件监听器也会累积。 用 EventEmitter 的时候反复 on 而不 off,或者在 HTTP 服务里对每个请求都注册一个监听器而请求结束后没摘掉,都会造成同样的问题。在堆快照里表现为 Listener 类对象数量异常。

heapdump 文件很大,别在生产机器上随手生成。 抓快照的瞬间进程会暂停几秒到几十秒,文件可能撑到几百 MB。生产环境要抓,提前规划好磁盘空间和抓取窗口,最好在预发环境复现。

写在最后

这次排查最大的收获不是那个定时器,而是一个思路:内存问题不要靠猜,靠对比。两份堆快照一减,该死的对象自己会站出来。代码层面能做的事情其实有限定时器记得清、缓存记得设上限、监听器记得摘、异步任务记得判重,这四条做到位,八成的 Node.js 内存增长都能挡住。剩下的,是给进程配一台内存够用的机器,让它有喘息的机会。

    jquery网页脚本库
    (7)jquery网页脚本库
    jquery网页脚本库,是一套比较齐全的JavaScript专业数据库特效合集,它集成了JavaScript网页特效编辑的通用功能,不论是BOM还是DOM都十分的齐全,用户可以利用它在网页上进行全面的特效编写。欢迎有需要的同学下载使用!关于jQuery:jQuery是一个快速、简洁的JavaScript框架,是继Prototype之后又一个优秀的JavaScript代码库(或JavaScript框架)。jQuery设计的宗旨是“writeLess,DoMore”,即倡导写更少的代码,做更多的事情。它...更多>>
    JavaScript编程辅助软件
    (6)JavaScript编程辅助软件
    JavaScript编程辅助软件,可以使你轻松快速的在网页中插入JavaScript代码。友好的用户界面,快捷的工具栏,智能的语法填充助手。方便鼠标单击文档导航条,丰富的分类源代码库,一切的设计都让你的效率倍增!应用产品特性:JavaScript编程辅助软件让你把静态的Web内容转成活泼、生动的互动应用。它有统一的JavaScript和HTML色彩编码语法、无限次复原和重做,让你可以做大胆的网页实验;它还具有完整的JavaScript参考指南,以及给初学者...更多>>

    相关评论

    阅读本文后您有什么感想? 已有人给出评价!

    • 8 喜欢喜欢
    • 3 顶顶
    • 1 难过难过
    • 5 囧囧
    • 3 围观围观
    • 2 无聊无聊

    发表评论

    昵称:
    表情: 高兴 可 汗 我不要 害羞 好 下下下 送花 屎 亲亲