NestJS后端无内存泄漏但RAM占满 进程内存异常排查
核心原因
你遇到的是Node.js服务非常典型的非堆内存泄漏问题,堆快照仅能统计V8垃圾回收器管理的堆内对象,占进程总内存(即top命令显示的RES常驻内存)的比例通常只有1/3到1/2,剩下的内存全部由V8之外的模块/系统层分配,完全不会被堆快照捕获,这也是你看到堆仅65MB、进程内存却冲到1.4G的根本原因。
结合你用Render部署NestJS、集成Puppeteer的场景,内存异常上涨的具体诱因按概率从高到低排序:
- Puppeteer/Chrome的残留内存未被正确清理
Chrome是多进程架构,你手动杀掉顶层Chrome进程只能释放很小一部分内存:如果启动Chrome时没加--disable-dev-shm-usage参数,Render容器默认极小的/dev/shm共享内存分区写满后,Chrome会直接往进程匿名内存页写数据,这部分内存由内核直接管理;另外如果调用Puppeteer后没执行await browser.close()做完整清理,Chrome和Node通信的IPC缓冲区、DevTools连接句柄、渲染进程残留的内存段都会挂靠在Node主进程下,既不会被V8回收,也不会在杀顶层Chrome进程时释放。你之前杀Chrome只释放80MB,基本可以确定是这部分残留内存占了大头。 - 堆外Buffer/Stream泄漏
Node.js里的Buffer、Stream处理大文件/大响应时,超过阈值的内存会直接通过libuv在系统层申请,不走V8堆。如果NestJS里的文件上传、接口代理、日志打印逻辑没正确销毁Stream、没释放Buffer引用,这部分内存会持续增长,堆快照完全无法检测到。 - 原生C扩展的内存泄漏
如果你在NestJS里用了bcrypt、sharp、canvas这类带原生C绑定的依赖,这些模块自行申请的内存不受V8管控,旧版本普遍存在未正确释放内存的问题,会直接推高进程RES。 - 容器内存回收机制的放大效应
Render容器默认用cgroup v1做资源隔离,Linux内核不会主动回收进程的匿名内存页,Node默认配置下就算释放了已申请的堆外内存,也不会把内存页还给操作系统,表现为内存只涨不跌,哪怕实际内存占用很低。
排查与修复方案
按以下顺序操作,基本可以定位并解决问题:
- 先明确内存构成
给启动命令加--expose-gc参数,在应用入口加定时内存打印逻辑,区分堆内、堆外内存占比:
如果setInterval(() => { const mem = process.memoryUsage(); console.table({ heapUsed: `${(mem.heapUsed / 1024 / 1024).toFixed(2)}MB`, external: `${(mem.external / 1024 / 1024).toFixed(2)}MB`, // V8统计的堆外内存(Buffer、原生模块分配) rss: `${(mem.rss / 1024 / 1024).toFixed(2)}MB` // 对应top显示的常驻内存 }); global.gc?.(); // 手动触发GC,排除GC延迟执行的干扰 }, 10000);rss - heapUsed差值超过800MB,可100%确定是非堆内存问题。 - 专项修复Puppeteer内存问题
- 启动Chrome时必须加
--disable-dev-shm-usage、--no-sandbox参数,禁止Chrome往共享内存分区写数据 - 所有Puppeteer调用逻辑必须加
try/finally,在finally块里执行await browser.close(),同时加30秒超时兜底,强制销毁超时未退出的浏览器实例 - 不要长期复用全局Browser实例,每处理10~20个请求就重建一次Browser实例,彻底释放残留内存
- 启动Chrome时必须加
- 调整Node启动参数限制内存无限制增长
把默认启动命令替换为以下内容,给V8设置合理的内存上限,触发更主动的内存回收:
该配置会把V8堆总上限限制在550MB左右,避免V8无限制向系统申请内存预留空间。node --max-old-space-size=512 --max-semi-space-size=16 dist/main.js - 排查堆外内存泄漏点
- 所有Stream处理逻辑必须监听
error、end事件,处理完成后主动调用stream.destroy()销毁流 - 全局缓存、日志拦截器不要存储Buffer、ArrayBuffer类型的大对象,转成字符串后再存储,方便V8追踪回收
- 升级所有带原生绑定的依赖到最新稳定版,排除依赖本身的内存泄漏问题
- 所有Stream处理逻辑必须监听
如果以上操作完成后内存仍然异常,可以在容器内安装smem工具,扫描Node进程的内存段分布,定位占用最大的匿名内存段归属,即可找到具体的泄漏模块。
内容的提问来源于stack exchange,提问作者JamesJGoodwin
相关产品推荐
相关产品推荐

