·更新于 2026年10月25日· 10 次浏览 Node.jsRedis内存泄漏heap snapshot工程实践

Node.js 博客服务内存缓慢上涨:我用 heap snapshot 找到未清理的 Redis 订阅和事件监听

一次上线后,Node.js 博客服务内存开始缓慢爬坡。通过 heap snapshot 对比,我定位到重复创建的 Redis 订阅和未移除的事件监听,补齐生命周期清理后,内存曲线恢复平稳。

Node.js 博客服务内存缓慢上涨:我用 heap snapshot 找到未清理的 Redis 订阅和事件监听 封面
ZHOUYI

作者

ZHOUYI

上周给博客服务上线了一个实时推荐提醒,第二天监控里的 Node.js 内存曲线开始缓慢爬坡:从三百多 MB 一路涨到九百多 MB,流量和接口耗时却没有明显变化。重启能暂时压下去,但过一天又涨回来。这种不是立刻爆炸的泄漏最容易被拖过去,我决定这次认真查到底。

先排除流量、缓存和数据库

我先对比了发版差异:Nuxt 前端只是多了一个订阅入口,MongoDB 查询计划和索引没有变化,Redis 的普通读写也没异常。因为内存是缓慢上涨,我更怀疑长生命周期对象没释放,比如定时器、连接、订阅、事件监听。于是先保留一份上涨后的 heap snapshot,再找机会复现。

heap snapshot 里的 Redis 订阅

用 node --inspect 启动服务,在 Chrome DevTools 里拍快照。按 Constructor 对比后,RedisSubscriber 和相关闭包数量持续增长;再看 Retainers,能追到页面推荐逻辑。问题是我在每次用户打开文章页时都创建了一个订阅,页面离开或请求取消时没有 unsubscribe,事件监听也没有移除。

javascript
const sub = redis.duplicate();
sub.subscribe('blog:recommend');
sub.on('message', (channel, payload) => {
  pushRecommend(payload);
});
function shutdown() {
  sub.unsubscribe('blog:recommend');
  sub.quit();
}
process.on('SIGTERM', shutdown);

把清理逻辑补到生命周期里

我把订阅改成服务启动时创建一次,而不是每次请求都新建;消息处理函数保持无状态,避免闭包抓住请求对象。对于确实需要请求级监听的地方,统一绑定 AbortSignal 或响应 close 事件,并在结束时 removeListener。这样业务代码不再依赖自觉,减少只写 on 不写 off 的情况。

  • 上线后先看内存趋势,不要只盯错误日志
  • heap snapshot 要对比两个时间点,重点看 Constructor 和 Retainers
  • Redis 订阅、EventEmitter、定时器都要有配对清理
  • 压测时模拟反复进入和离开页面,才能暴露缓慢泄漏

修完后再压测并观察两天,内存稳定在原来的水位。这个坑给我的提醒很直接:Node.js 服务不是没存全局变量就安全,任何长连接、订阅和监听都要有明确出生和死亡。不同 Redis 客户端 API 细节可能不同,具体以官方文档为准。

Guestbook

留言

若这篇文章也曾停在你心上,留一行字吧,我会看的。

先确认一下邮箱

多这一步,是为了少些机器骚扰,也让我能更踏实地读每一条真人写下的字。验证码 10 分钟内有效;同一邮箱 60 秒内只能发一封。

本文留言

共 0 条已加载 0 条

这里还静悄悄的——愿意做第一个留下脚印的人吗?