← 返回题目列表

SSR 服务为什么可能出现内存泄漏?如何排查和预防?

困难 第 22 / 25 题 更新于 2026/07/29
SSR内存泄漏Node.js稳定性

简化版

SSR 服务是长期运行的进程,如果在模块级变量、全局缓存、定时器、事件监听、请求上下文、流对象里保留了本该随请求释放的数据,就可能内存泄漏。排查时要看内存曲线、堆快照、对象保留路径和发布关联;预防时要避免全局存用户数据,给缓存设置容量和 TTL,请求结束后清理资源,并用压测观察多轮请求后的内存是否回落。

详细版

和纯静态站不同,SSR server 会持续处理请求。一次请求里创建的用户信息、接口结果、组件上下文,如果被全局引用保存下来,就无法被垃圾回收。

常见原因包括:为了优化性能做了无上限 Map 缓存;在模块单例中存了 request;每次请求都注册事件监听但不移除;流式渲染异常中断后没有关闭资源;定时器捕获了大对象。

面试中要强调“泄漏不是内存高,而是请求结束后对象仍被引用,并且多轮流量后持续增长”。

完整版教学

一、SSR 内存泄漏的本质

Node.js 进程会复用。

模块级变量会跨请求存在。

如果某个请求对象被全局结构引用,它就不会随请求结束释放。

流量越大,泄漏越快表现为内存上涨、GC 频繁、响应变慢,最后进程 OOM。

二、常见泄漏来源

来源例子风险
全局缓存new Map() 无 TTL数据无限增长
请求上下文把 user/request 存到单例用户数据串请求
事件监听每次请求 onofflistener 累积
定时器interval 捕获大对象长期无法释放
流对象异常时未关闭buffer 堆积
三方库单例内部缓存不易观察

三、代码示例

const cache = new Map<string, unknown>();

export async function render(req) {
  const user = await getUser(req);
  // 问题:如果 key 无限增长,user 会长期留在进程里
  cache.set(req.url + user.id, user);
  return renderHtml(user);
}

如果确实需要缓存,应该设置容量、TTL 和淘汰策略,并避免存放敏感用户对象。

四、排查步骤

先看内存曲线是否持续上升。

再区分是正常缓存升高还是泄漏。

用压测复现多轮请求,看流量停止后内存是否回落。

抓取 heap snapshot,对比增长对象。

查看 retaining path,找到是谁持有引用。

排查内存泄漏时,不要只看某一刻内存高低,要看“增长趋势”和“是否可回收”。

五、和缓存的边界

SSR 缓存能提升性能,但缓存必须有边界。

页面级缓存可以放 CDN 或外部缓存,减少进程内缓存压力。

进程内缓存只适合小规模、低敏感、可淘汰的数据。

用户级数据不要随意放全局缓存。

六、预防策略

代码评审关注模块级可变状态。

封装 request context,确保只在请求生命周期内使用。

事件监听用完移除。

超时、取消和异常路径都要释放资源。

上线前用压测观察内存平台期。

七、误区和追问

  • 误区:Node.js 内存上涨就是泄漏。 V8 可能延迟 GC,关键看停止流量后是否回落和对象是否仍被引用。
  • 误区:SSR 页面是前端,不用管服务内存。 SSR 服务就是后端进程,稳定性责任更接近服务端。
  • 误区:缓存越多性能越好。 无边界缓存会把性能优化变成稳定性事故。
  • 追问:怎么定位是哪个对象泄漏? 对比 heap snapshot,看增长对象和 retaining path。
  • 追问:为什么全局变量危险? 它跨请求存在,容易把一次请求的数据保留到进程生命周期。
  • 追问:临时重启能解决吗? 只能止血,不能解决泄漏源,流量回来还会复发。

八、面试总结口径

回答时按“现象、原因、排查、预防、兜底”讲。

兜底可以包括进程守护、内存阈值重启、限流和降级,但这些不能替代修复根因。