跳到主要内容

认识

2024年12月20日
柏拉文
越努力,越幸运

一、认识


Node.js 中,内存泄漏 指的是程序在执行过程中 未能正确释放不再使用 的内存,从而导致 内存持续增长并最终耗尽系统资源。内存泄漏会影响程序的性能,导致应用变慢,甚至崩溃。

二、场景


  1. 定时器: setTimeout 没有被清理,导致引用持续存在,内存无法释放。需要使用 clearTimeout 清理定时器; setInterval 没有被清理,导致引用持续存在,内存无法释放。需要使用 clearInterval 清理定时器

  2. 闭包引用: 闭包中对变量的引用被保留,导致内存无法被释放。避免不必要的闭包引用,确保及时释放大对象。

  3. 数组引用: 数组持续通过 pushappend 等操作新增、插入数据,而由于存在强引用,这些对象无法被垃圾回收器(GC)清理。所以导致,请求越多,那么 leaks 占用的内存越来越大。

  4. 事件监听器: EventEmitter 添加事件监听器后,未适时移除,导致事件堆积,无法释放内存。

  5. 未关闭资源句柄: fs.open() 返回的文件句柄一定要通过 fs.close 关闭; net.createServer() 返回的网络服务器句柄; zlib.createGzip() 返回的压缩句柄; dgram.createSocket() 返回的 UDP socket 句柄; crypto.createHash() 返回的哈希句柄; child_process.spawn() 返回的子进程句柄

  6. 错误处理不完善: 异步回调函数出现错误,但未正确释放资源。添加完善的错误处理,确保资源总能被释放。

  7. 强引用 Set/Map

  8. 缓存数据无限存储: 应用程序缓存过多的数据,导致内存不断占用。

三、定位


测试环境: 通过 node --inspect 开启 Node 调试模式, 打开 Chrome 浏览器,在地址栏输入 chrome://inspect, 打开专用 DevTools 窗口, 根据可疑时间点, 录制内存快照(Heap Snapshot)。 通过 Summary 或者 Comparison 来定位可疑对象。在 Comparison 视图中选择两个堆快照,并在它们之间进行比较。您可以查看哪些对象在两个堆快照之间新增,哪些对象在两个堆快照之间减少,以及哪些对象的大小发生了变化。Comparison 视图还允许查看对象之间的关系,以及对象的详细信息,如类型、大小和引用计数。通过这些信息,可以了解哪些对象是导致内存泄漏的原因。在 Comparison 视图中查找可疑对象,用于比较当前快照和之前快照的内存变化。主要关注 Comparison.Delta 对象数量的净增量和 Comparison.Size Delta 对象分配的内存变化量。然后通过 Comparison.Retainers 引用链 显示某个对象被哪些对象引用,为什么无法被回收。

生产环境: heapdump 模块可以在运行时生成堆快照。堆快照可导入到 Chrome DevTools 进行分析,找到泄漏原因。监控内存使用情况,自动生成 heap snapshot,并将快照上传到远程服务器。它还包含了手动生成 heap snapshot 和触发垃圾回收(GC)的功能,帮助开发者定位和排查内存泄漏问题。

  1. 应用启动时, 生成启动的 Heapdump 快照, 并开始定时监控内存使用情况

  2. 针对可疑路由添加 memoryUsageMiddlewaretrackRequestCountMiddleware 中间件。memoryUsageMiddleware:定期检查内存使用情况并触发生成 heap snapshot, 比如内存每达到 100M 生成一次 heap snapshottrackRequestCountMiddleware:根据请求计数触发 heap snapshot, 比如每 请求 100 次生成一次 heap snapshot

  3. 生成的 heap snapshot 会保存在本地目录 heapdump, 并通过 scp 上传到指定的远程服务器目录。生成 heap snapshot 会主动调用 gc。通过 v8 global.gc() 允许你主动触发垃圾回收,让 V8 进行内存清理。这对于 诊断内存泄漏生成更准确的堆快照 非常有用。在某些场景下,通过手动触发 GC,可以释放不再使用的内存,确保堆快照中只保留真实的内存占用情况, 如果手动触发 GC 后,内存仍然无法释放,说明存在未被回收的对象,即可能存在内存泄漏。

  4. 提供了 /heap-snapshot 接口,开发者可以手动触发 heap snapshot 的生成。提供了 /trigger-gc 开发者可以手动触发垃圾回收。

但是在生产环境中, Heapdump 生成快照 会消耗大量的内存和 CPU,从而会影响服务的正常运行。有时候, Node 服务会中断,根据当时服务器内存大小这个时间会在 2 ~ 30min 左右。确保服务高可用性和生成 Heapdump 快照的核心策略是将快照生成的资源消耗隔离。

  1. 可以通过扩展 Pod 副本数,通过负载均衡消除单个 Pod 性能下降的影响。: 通过 ServiceNode.js 服务暴露给外部访问,并结合 Deployment ReadinessProbe 确保 Service 仅将流量分发给健康的 Pod, 保证服务稳定和服务高可用。配置Deployment replicas, 配置 Node.js Pod 多个副本, 在生成 Heapdump 之前,通过 Kubernetes 修改 PodreadinessProbe,让流量从负载均衡中移除, 将当前 Pod 的流量隔离,防止因性能下降影响请求响应。这样即使某个 Pod 性能受损,服务流量会自动分配到其他健康的 Pod,保证服务的高可用性。

  2. 可以使用 child_process 独立进程分离快照生成逻辑,避免主服务受影响。: 将生成 Heapdump 的任务交给一个独立的进程,避免主服务受到影响。快照生成由独立的脚本或服务运行。主服务通过异步调用启动快照生成服务。

  3. 可以使用 Job 或 分离快照生成逻辑,避免主服务受影响。: 通过 KubernetesJobCronJob 创建临时 Pod 专门用于生成 Heapdump,而不是在主服务 Pod 中生成。Heapdump 的生成在独立的 Pod 中进行,完全不影响主服务。