Node.js 内存泄漏排查实战指南

发布于2026-08-16 11:32 阅读10次 系统讲解 Node.js 内存泄漏的排查方法:从识别泄漏征兆、采集堆快照、Chrome DevTools 对比分析,到闭包、事件监听器、定时器、缓存无限增长等常见泄漏场景的修复,附完整代码示例和压测验证流程。
# Node.js 内存泄漏排查实战指南
## 前言
Node.js 服务跑久了内存慢慢涨、最终 OOM 崩溃,是很多后端开发者都遇到过的头疼问题。内存泄漏不像报错那样直接暴露,往往要运行几小时甚至几天才显现。本文整理一套完整的排查方法和实战案例。
## 内存泄漏的典型征兆
- 服务内存占用随时间持续上升,且不会回落
- 定时 GC 后内存仍居高不下
- 用户量增加时,内存涨幅远超预期
- 频繁触发 OOM,进程被系统 kill
先确认是不是真的泄漏:有时内存上涨只是因为缓存预热、连接池扩容,属于正常现象。
## 第一步:采集堆快照
在疑似泄漏的时间点,采集多份堆快照做对比。
```javascript
// 方式一:运行时用 inspector 模块手动触发
const inspector = require("inspector");
const fs = require("fs");
function takeHeapSnapshot() {
const session = new inspector.Session();
session.connect();
const fd = fs.openSync(`./heap-${Date.now()}.heapsnapshot`, "w");
session.on("HeapProfiler.addHeapSnapshotChunk", (m) => {
fs.writeSync(fd, m.params.chunk);
});
session.post("HeapProfiler.takeHeapSnapshot", () => {
session.disconnect();
fs.closeSync(fd);
});
}
```
生产环境更推荐用 `--inspect` 启动,通过 Chrome DevTools 的 Memory 面板远程采集:
```bash
node --inspect=0.0.0.0:9229 server.js
```
## 第二步:对比快照找增长点
在 Chrome DevTools 中:
1. 打开 Memory 面板 → Heap snapshot
2. 采集第一份快照,等一段时间(或压测)
3. 采集第二份快照
4. 切换视图到 Comparison(对比)模式
5. 按 Delta 排序,找新增对象最多的构造函数
关注这几个常见元凶:
- **Closure(闭包)**:闭包引用大对象未释放
- **EventEmitter**:监听器不断增加
- **Array / Object**:全局缓存无限增长
- **Socket / Stream**:连接未正确销毁
## 常见泄漏场景与修复
### 场景1:全局缓存无限增长
```javascript
// 错误:缓存不设上限,永不清理
const cache = new Map();
function getData(key) {
if (!cache.has(key)) {
cache.set(key, expensiveCompute(key));
}
return cache.get(key);
}
// 修复:用 LRU 缓存,限制容量
const LRU = require("lru-cache");
const cache = new LRU({
max: 500, // 最多500条
ttl: 1000 * 60 * 5, // 5分钟过期
});
```
### 场景2:事件监听器未移除
```javascript
// 错误:每次请求都添加监听器
function handleRequest(req) {
emitter.on("data", (d) => process(d));
}
// 修复:用 once 或显式移除
function handleRequest(req) {
const handler = (d) => {
process(d);
emitter.removeListener("data", handler);
};
emitter.on("data", handler);
}
// 更好的方式:emitter.once("data", handler)
```
可以用 `process.getMaxListeners` 检查,配合 `events.setMaxListeners` 防止监听器堆积。
### 场景3:定时器未清理
```javascript
// 错误:setInterval 未清除
setInterval(() => checkStatus(), 1000);
// 修复:保存引用,用完清除
const timer = setInterval(() => checkStatus(), 1000);
// 需要停止时
clearInterval(timer);
```
### 场景4:数据库连接未释放
```javascript
// 错误:每个请求新建连接但不关闭
const client = new MongoClient(url);
await client.connect();
// 用完没 close,连接对象累积
// 修复:用连接池(客户端内置),复用连接
// 并在应用退出时统一关闭
process.on("SIGTERM", async () => {
await client.close();
process.exit(0);
});
```
## 第三步:用 heapdump + Chrome 分析
```bash
npm install heapdump
```
```javascript
// 在需要的地方触发快照
const heapdump = require("heapdump");
heapdump.writeSnapshot("./snapshot.heapsnapshot");
```
然后 Chrome DevTools → Memory → Load 加载快照文件分析。
## 第四步:压测验证
修复后用工具压测,观察内存曲线是否平稳:
```bash
# 用 autocannon 压测
npx autocannon -c 100 -d 60 http://localhost:3000
# 或用 wrk
wrk -t12 -c400 -d60s http://localhost:3000
```
压测同时用 `process.memoryUsage()` 记录:
```javascript
setInterval(() => {
const m = process.memoryUsage();
console.log("heapUsed:", Math.round(m.heapUsed / 1024 / 1024) + "MB");
}, 5000);
```
如果压测后 heapUsed 能回落,说明泄漏已修复。
## 排查流程总结
| 步骤 | 操作 |
|------|------|
| 1 | 确认是否真泄漏 |
| 2 | 采集多份堆快照 |
| 3 | Comparison 对比找增长 |
| 4 | 定位到具体代码 |
| 5 | 修复(缓存上限/移除监听/清理定时器) |
| 6 | 压测验证 |
## 小结
内存泄漏排查靠的是"对比"而非"猜":多份快照对比、压测前后对比。掌握 heap snapshot 和 Comparison 视图,配合对闭包、监听器、定时器、缓存这几类常见元凶的敏感度,大部分泄漏都能快速定位。