Nodejs内存泄漏排查
1. V8 内存模型
进程内存
├── New Space(新生代) ~32MB,频繁 minor GC
├── Old Space(老生代) 长寿对象
├── Code Space JIT 代码
├── Map Space 对象 hidden class
└── Large Object Space > 1MB 对象
启动时设:
node --max-old-space-size=2048 server.js # 老生代上限 2GB
node --max-semi-space-size=64 server.js # 新生代单 space 上限 64MB
容器内:max-old-space-size 设到容器 memory limit 的 75%。
2. 泄漏的典型表现
# 看进程内存
ps aux | grep node | awk '{print $4, $5, $6, $11}'
# %MEM RSS VSZ COMMAND
健康:稳定波动 / 缓慢增长后被 GC 回收。 泄漏:持续单调上升直到 OOM。
// 应用内监控
setInterval(() => {
const m = process.memoryUsage()
logger.info({
rss: m.rss / 1024 / 1024,
heapTotal: m.heapTotal / 1024 / 1024,
heapUsed: m.heapUsed / 1024 / 1024,
external: m.external / 1024 / 1024,
}, 'memory')
}, 30000)
3. 常见泄漏模式
3.1 全局变量 / 闭包
// 全局 cache 无限增长
const cache = {}
app.get('/user/:id', (req, res) => {
cache[req.params.id] = await db.query(...)
// 没清理
})
// 修复:用 LRU
const LRU = require('lru-cache')
const cache = new LRU({ max: 1000, ttl: 1000 * 60 * 5 })
3.2 事件监听器
// ✗ 每次请求注册,不解绑
app.use((req, res, next) => {
process.on('uncaughtException', () => {}) // 每请求加一个
next()
})
// 看监听器数量
emitter.listenerCount('event')
emitter.setMaxListeners(20) // 默认 10,超就警告
3.3 Timer / Interval 没清
// ✗ 类销毁时 timer 还在
class Worker {
constructor() {
this.timer = setInterval(() => this.tick(), 1000)
}
destroy() {
// 忘了 clearInterval
}
}
3.4 大对象长驻
// 全局历史记录无上限
const history = []
app.use((req, res, next) => {
history.push({ req, res }) // res 含整个响应
next()
})
3.5 Promise 链不结束
// 长连接里 Promise 一直挂着
async function watchFile() {
while (true) {
await new Promise(resolve => fs.watch('/log', resolve))
// 永不 resolve 的 Promise + 长时间引用
}
}
4. heapdump 抓堆快照
4.1 用 --inspect
node --inspect=0.0.0.0:9229 server.js
# K8s port-forward
kubectl port-forward pod/myapp 9229:9229
Chrome 打开 chrome://inspect → Connect → Memory tab → "Take heap snapshot"。
4.2 用 v8 模块
const v8 = require('v8')
const fs = require('fs')
function dump() {
const filename = `/tmp/heap-${Date.now()}.heapsnapshot`
const stream = v8.getHeapSnapshot()
stream.pipe(fs.createWriteStream(filename))
return filename
}
// SIGUSR2 触发
process.on('SIGUSR2', () => dump())
kill -SIGUSR2 <pid>
把 .heapsnapshot 拖进 Chrome DevTools → Memory 分析。
4.3 对比两次快照
- 启动后取 snapshot 1
- 跑业务一段时间
- 触发 GC(DevTools 垃圾桶按钮)
- 取 snapshot 2
- Comparison 模式找新增对象
5. clinic.js 堆分析
npx clinic heap -- node server.js
# Ctrl+C 后产出报告
可视化展示每类对象内存增长。
6. 实战排查流程
6.1 现象确认
# 看 Pod 内存增长趋势(Grafana)
container_memory_working_set_bytes{pod="frontend-xxx"}
# 看进程
top -p <pid>
# RES 字段持续涨
6.2 GC 日志
node --trace-gc --trace-gc-verbose server.js
# 看 GC 频率和回收量
# 频繁 full GC 但 heap 不降 = 老生代有不可回收对象
6.3 取堆快照分析
按"3.4 抓堆快照"。
DevTools 里找:
- Retained Size 大的对象类型
- "Detached"(已脱离 DOM 但还引用)
- 数量异常多的对象(
MapArray几百万个)
6.4 修复并验证
修复后再跑相同场景,看快照对象数是否回落。
7. 容器场景
容器内 process.memoryUsage() 是进程数据,但 os.totalmem() 在 Node 18 前是宿主机数据,会误判。
K8s 限制 + Node 设:
resources:
limits:
memory: "2Gi"
node --max-old-space-size=1536 server.js # 1.5G < 2G 留头
OOM Killed 的 Pod:
kubectl describe pod <pod>
# Last State: Terminated
# Reason: OOMKilled
8. 第三方库内存问题
mongoose连接没关axios没用同一 instance(每请求新 agent)puppeteerbrowser 没 close- 旧版
moment替换为 dayjs
9. 常见反模式
global当 cache:永不 GC- listener 不 off:每次都注册新的
- 错误处理里 setInterval 重试:失败链堆积
- 大 buffer 不流式:fs.readFile 整个文件进内存
- 没设 max-old-space-size:容器 limit 内 V8 还想用更多就 OOM
- 生产开 --inspect:暴露调试端口
- 不监控 RSS:等 OOM 才发现