如何调试 Node.js 生产应用中的内存泄漏

发布: (2026年4月1日 GMT+8 15:50)
4 分钟阅读
原文: Dev.to

Source: Dev.to

确认是泄漏

# 监控 Node 进程的 RSS 内存随时间的变化
watch -n 30 'ps -o pid,rss,vsz,comm -p $(pgrep -f "node server")'

# 或者在应用内部记录
setInterval(() => {
  const m = process.memoryUsage();
  console.log(JSON.stringify({
    rss: Math.round(m.rss / 1024 / 1024) + 'MB',
    heap: Math.round(m.heapUsed / 1024 / 1024) + 'MB',
    time: new Date().toISOString()
  }));
}, 60000);

如果 RSS 持续增长且从不下降,则说明存在泄漏。

步骤 1:生成堆快照

# 使用 inspector 启动 Node
node --inspect server.js

# 或者向正在运行的进程发送信号
kill -USR1   # 在 9229 端口打开 inspector

随后在 Chrome 中打开 chrome://inspect → 打开专用 DevTools → MemoryTake heap snapshot
拍摄一次快照,执行一些操作,再拍摄另一张快照并进行比较——寻找不断累积的对象。

步骤 2:使用 clinic.js(最简方法)

npm install -g clinic

# 在负载下对应用进行分析
clinic heapprofile -- node server.js

# 在另一个终端运行负载
npx autocannon -c 10 -d 60 http://localhost:3000/api/endpoint

# 按 Ctrl+C 停止 clinic —— 它会生成 flamegraph

flamegraph 显示内存分配的来源。高的柱状条表示分配量大的函数。

步骤 3:常见泄漏模式

模式 1:事件监听器累积

// 泄漏:每个请求都添加监听器却不移除
app.get('/stream', (req, res) => {
  emitter.on('data', (chunk) => res.write(chunk)); // 从未移除!
});

// 修复:在连接关闭时移除监听器
app.get('/stream', (req, res) => {
  const handler = (chunk) => res.write(chunk);
  emitter.on('data', handler);
  req.on('close', () => emitter.off('data', handler)); // 清理
});

模式 2:没有过期时间的全局缓存

// 泄漏:缓存无限增长
const cache = {};
app.get('/user/:id', async (req, res) => {
  if (!cache[req.params.id]) {
    cache[req.params.id] = await db.getUser(req.params.id);
  }
  res.json(cache[req.params.id]);
});

// 修复:使用带 TTL 的合适缓存
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 300, maxKeys: 1000 });

模式 3:闭包持有引用

// 泄漏:闭包让大对象保持在内存中
function processData(largeArray) {
  const summary = computeSummary(largeArray);
  return function getSummary() {
    return summary; // largeArray 通过闭包仍在内存中
  };
}

// 修复:只保留需要的内容
function processData(largeArray) {
  const summary = computeSummary(largeArray);
  largeArray = null; // 帮助 GC
  return function getSummary() { return summary; };
}

模式 4:未清除的定时器

// 泄漏:interval 从未清除
function startMonitoring() {
  setInterval(() => {
    checkHealth();
  }, 5000); // 没有保存引用,无法清除
}

// 修复:保存引用并在关闭时清除
let monitorInterval;
function startMonitoring() {
  monitorInterval = setInterval(() => checkHealth(), 5000);
}
process.on('SIGTERM', () => clearInterval(monitorInterval));

步骤 4:使用自动化测试检测

// 添加到你的测试套件中
const v8 = require('v8');

test('endpoint does not leak memory', async () => {
  global.gc(); // 强制 GC(使用 --expose-gc 运行)
  const before = v8.getHeapStatistics().used_heap_size;

  // 将操作运行 100 次
  for (let i = 0; i < 100; i++) {
    await request(app).get('/api/users');
  }

  global.gc();
  const after = v8.getHeapStatistics().used_heap_size;

  const growthMB = (after - before) / 1024 / 1024;
  expect(growthMB).toBeLessThan(5); // 允许增长 <5MB
});
0 浏览
Back to Blog

相关文章

阅读更多 »