“线上问题没有小问题,尤其是那种偶发的、不好重现的,最让人头大。”
一、背景介绍
最近我们团队在做一个高并发的接口服务,技术栈是Node.js+Express。上线后,随着访问量的提升,服务突然开始频繁重启,日志里全是“JavaScript heap out of memory”。一开始以为是服务器内存小,结果加了内存还是会挂。很明显,遇到了传说中的——内存泄漏。
二、问题现象
服务刚启动时一切正常,内存占用也不高。但只要一有流量,内存就会慢慢上涨。每次到1.5GB左右,Node进程就崩溃重启。更郁闷的是,重启后又能跑一会儿,再次爆掉,死循环……
三、排查思路
说实话,第一次遇到这种问题,真有点慌。但冷静下来,其实排查思路很清晰:
- 确认问题是否必现:用压测工具模拟流量,发现内存确实一直涨。
- 定位泄漏点:用Node.js自带的
--inspect
参数配合Chrome DevTools,抓内存快照,分析堆内对象。 - 怀疑第三方依赖:排查了下,发现我们用了一些缓存库和中间件,怀疑有问题。
- 代码自查:重点检查了全局变量、闭包、定时器等常见泄漏点。
四、核心排查过程
1. 利用Chrome DevTools抓内存快照
在服务启动参数里加上--inspect
,然后用Chrome访问chrome://inspect
,连接到Node进程。压测跑一会儿,抓一张快照,再过一会儿再抓一张,比较两次的对象数量和类型。
(图示建议:插一张DevTools内存快照对比的截图)
2. 发现泄漏对象
分析快照发现,有大量的Buffer对象和某个自定义Cache对象一直没被释放。进一步排查代码,发现是有个缓存模块,写了个Map存储请求结果,但忘了加淘汰策略,导致缓存一直涨。
// 问题代码片段
const cache = new Map();
app.get('/api/data', (req, res) => {
const key = req.query.id;
if (cache.has(key)) {
return res.json(cache.get(key));
}
// 省略真实查询逻辑
cache.set(key, result);
res.json(result);
});
只要有新请求,cache就会越来越大,永远不清理。
3. 修复方案
给缓存加上最大容量,超出就淘汰最早的:
const MAX_CACHE_SIZE = 1000;
const cache = new Map();
app.get('/api/data', (req, res) => {
const key = req.query.id;
if (cache.has(key)) {
return res.json(cache.get(key));
}
// 省略真实查询逻辑
cache.set(key, result);
// 淘汰策略
if (cache.size > MAX_CACHE_SIZE) {
const oldestKey = cache.keys().next().value;
cache.delete(oldestKey);
}
res.json(result);
});
修复后再次压测,内存占用明显稳定,服务不再频繁重启。
五、经验总结
- 线上服务要监控内存曲线,及时发现异常。
- 缓存一定要有淘汰策略,别偷懒。
- 用好Node.js的内存分析工具,定位泄漏对象。
- 多做压测和对比快照,不要只靠猜。
六、写在最后
这次内存泄漏虽然折腾了几天,但也让我对Node.js的内存机制和调试工具有了更深的理解。线上环境容不得半点侥幸,大家平时写代码一定要多留心,别让小问题变成大事故。