[测试] Node.js 进程内存泄漏排查:从 heapdump 到根因修复
前言最近在生产环境遇到一个 Node.js 服务内存持续增长的问题RSS 从启动时的 120MB 在 72 小时内涨到 1.2GB触发了 OOM Killer。说实话内存泄漏排查是后端开发中最让人头疼的问题之一这次踩了不少坑记录一下完整的排查过程。## 1. 现象确认通过 Prometheus 监控发现process_resident_memory_bytes呈线性增长且 GC 后不回落javascript// 快速确认在进程内打印堆内存setInterval(() { const mem process.memoryUsage(); console.log({ rss: (mem.rss / 1024 / 1024).toFixed(1) MB, heapUsed: (mem.heapUsed / 1024 / 1024).toFixed(1) MB, external: (mem.external / 1024 / 1024).toFixed(1) MB, });}, 30000);观察发现heapUsed在稳步增长说明是 JS 堆内泄漏而非 native 插件问题。## 2. 抓取 Heapdump使用v8.writeHeapSnapshot()在不同时间点抓两份快照进行对比javascriptconst v8 require(v8);// 启动 5 分钟后抓第一份setTimeout(() { v8.writeHeapSnapshot(/tmp/heap-t1.heapsnapshot);}, 5 * 60 * 1000);// 启动 30 分钟后抓第二份setTimeout(() { v8.writeHeapSnapshot(/tmp/heap-t2.heapsnapshot);}, 30 * 60 * 1000);## 3. Chrome DevTools 对比分析在 Chrome DevTools → Memory 面板加载两份快照选择Comparison视图按Delta排序。关键发现-(string)类型增量最大48000 个对象- Retainer 路径指向一个Map对象key 是请求 ID- 这个 Map 挂在一个全局的requestCache上## 4. 根因定位追踪到代码中一个请求缓存只有 set 没有 deletejavascript// 问题代码只进不出的缓存const requestCache new Map();app.use((req, res, next) { const reqId generateId(); requestCache.set(reqId, { url: req.url, startTime: Date.now(), headers: req.headers, // 每个请求的 headers 对象 ~2KB }); next();});每个请求往 Map 里塞约 2KB 数据QPS 100 的情况下每小时增长 ~700MB。## 5. 修复方案个人觉得最简单有效的方案是加 TTL 淘汰javascript// 修复加 TTL 定期清理const requestCache new Map();const CACHE_TTL 60 * 1000; // 1 分钟setInterval(() { const now Date.now(); for (const [key, val] of requestCache) { if (now - val.startTime CACHE_TTL) { requestCache.delete(key); } }}, 30 * 1000);部署后 RSS 稳定在 150MB 左右72 小时无增长。## 小结内存泄漏排查的核心步骤监控确认 → heapdump 对比 → Retainer 追踪 → 定位无界增长的数据结构。大多数 Node.js 内存泄漏都是因为某个集合Map / Set / Array只增不减加个 TTL 或 LRU 就能解决。