如何调试 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 → Memory → Take 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 —— 它会生成 flamegraphflamegraph 显示内存分配的来源。高的柱状条表示分配量大的函数。
步骤 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
});