记一次Node内存泄漏排查

1,042 阅读4分钟

MTY4ODg1Njc3MzI1MTE0Nw_115991_ttuwL1t1LUMNKkLw_1650444826.png

前言

这篇来自于4月份的笔记,起因是项目中发现 ssr 服务内存 2 周内从 20% 涨到 90%,有很明显的内存泄漏。解决问题之前先搜搜哪些常见的原因

  1. 缓存:有意的全局缓存,可以使用 lru-cache 优化缓存策略。
  2. 全局变量:无意的全局数组、对象,尽量不使用全局变量。
  3. 闭包:正常的使用闭包不会造成内存泄漏,错误的使用会。

先了解可能的原因,然后在排查的过程中重点关注这些点,是解决问题的常规思路。

生成snapshot

nodejs 通过生成 heap snapshot,然后在chrome 的devtools > memory导入进行分析

MTY4ODg1Njc3MzI1MTE0Nw_916748_xfNTJe6fSU7bWghg_1650450335.png

本地生成

node --inspect index.js

打开chrome://inspect 找到服务,点 inspect,然后就可以在 memory 生成快照了

远程生成

可以提供一个接口,调用接口打印,再从容器中捞出来进行分析,比较方便。

import { writeHeapSnapshot } from 'v8';
export default async function handler() {
  gc(); // 需要运行时添加 --expose-gc
  writeHeapSnapshot();
}

分析

分析,我们主要是要考虑如何合理的打印snapshot,以及如何借助工具分析snapshot。

关于合理打印snapshot,我在排查问题过程中总结的经验是:

使用 apache ab进行压测,不要持续压(take snapshot1 > 压测 > take snapshot1 > 空闲 > take snapshot2 > 压测 > take snapshot3),按阶段生成,方便对比snapshot

ab -n 1000 -c 10 http://localhost:3000/xxx

接下来通过示例代码,打印快照简单介绍一下分析工具 memory的 4 个视图

// 示例代码
function test1 () {
  versions = new Array(100000)
}
test1()
console.log(versions)
// 闭包
let foo = null
function outer () {
  const bar = foo
  function unused () { // 未使用到的函数
    console.log(`bar is ${bar}`)
  }
  foo = { // 给foo变量重新赋值
    bigData: new Array(100000).join('this_is_a_big_data'), // 如果这个对象携带的数据非常大,将会造成非常大的内存泄漏
    inner: function () {
      console.log('inner method run')
    }
  }
}
setInterval(outer, 100)

这是一段模拟闭包和全局变量类型内存泄漏的代码。

在目前的 V8 实现当中,闭包对象是当前作用域中的所有内部函数作用域共享的,也就是说 foo.inner 和 unUsed 共享同一个闭包的 context,导致 foo.inner 隐式的持有了对之前的 bar 的引用,形成了链。

summary 视图

根据构造函数名分组,可以查看个构造函数的内存占用大小,也可以查询两次快照之间新申请的占用。追踪dom泄漏特别有用。

  1. shallow size表示自己占多大内存
  2. retained size表示回收掉自己会回收多少。通常 retained size 大的比较危险。

image.png

可以发现 closure 和 Object 有异常

comparison 视图

比较两个快照,查看增量定位内存泄漏问题。

  1. Delta:数量变化
  2. Size Delta:内存变化

可以发现 array,concatenated string,closure 有异常

containment视图

提供对象结构视图,分析全局对象,分析闭包。

可以发现 versions 变量占用较多内存(非泄漏,测试用)

stastics视图

饼图,查看各类型占比。

到此我们了解了各种原因的内存泄漏应该主要使用哪种视图排查,当然这不是绝对的。

排查

demo 分析比较容易,但业务问题就复杂多了,不像我们在 demo 中带着答案去找问题。

  1. 线上业务问题往往是未知的,不是你写的代码,不是你维护的模块。
  2. 服务数量未知,不一定是哪个接口导致的内存泄漏。
  3. 第三方库,不了解的业务逻辑,都会对排查问题产生阻碍。
  4. 内存快照的不同类型元素的数量往往很多,都是上万级别的,有工具也不好筛查。

Daletnd Deav Alcc Sre Froed Sire Spe Dela.png

通过comparison视图,我们发现这个String异常的多,接着我们发现task_info_list>app_welfare_info_list>treecache的缓存一直在涨,而这个缓存来自第三方状态管理库 recoil...

selector 会有缓存,这个特性在 CSR 中不会有问题,但在 SSR 中,缓存堆积会造成内存泄漏。官方提供了方案,修改缓存策略为most-recent。selector(options) | Recoil

cachePolicy_UNSTABLE: {
    // Only store the most recent set of dependencies and their values
    eviction: 'most-recent',
  }

修复后观察,内存保持平稳。

总结

查找问题过程中,注意生成快照的时机和次数,利用好4个视图的特点,利用comparison,找到增长的基础构造函数。利用 containment,分析全局变量和闭包。

参考

  1. developer.chrome.com/docs/devtoo…
  2. 有意思的 Node.js 内存泄漏问题 - 云+社区 - 腾讯云