前言
这篇来自于4月份的笔记,起因是项目中发现 ssr 服务内存 2 周内从 20% 涨到 90%,有很明显的内存泄漏。解决问题之前先搜搜哪些常见的原因
- 缓存:有意的全局缓存,可以使用 lru-cache 优化缓存策略。
- 全局变量:无意的全局数组、对象,尽量不使用全局变量。
- 闭包:正常的使用闭包不会造成内存泄漏,错误的使用会。
先了解可能的原因,然后在排查的过程中重点关注这些点,是解决问题的常规思路。
生成snapshot
nodejs 通过生成 heap snapshot,然后在chrome 的devtools > memory导入进行分析
本地生成
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泄漏特别有用。
- shallow size表示自己占多大内存
- retained size表示回收掉自己会回收多少。通常 retained size 大的比较危险。
可以发现 closure 和 Object 有异常
comparison 视图
比较两个快照,查看增量定位内存泄漏问题。
- Delta:数量变化
- Size Delta:内存变化
可以发现 array,concatenated string,closure 有异常
containment视图
提供对象结构视图,分析全局对象,分析闭包。
可以发现 versions 变量占用较多内存(非泄漏,测试用)
stastics视图
饼图,查看各类型占比。
到此我们了解了各种原因的内存泄漏应该主要使用哪种视图排查,当然这不是绝对的。
排查
demo 分析比较容易,但业务问题就复杂多了,不像我们在 demo 中带着答案去找问题。
- 线上业务问题往往是未知的,不是你写的代码,不是你维护的模块。
- 服务数量未知,不一定是哪个接口导致的内存泄漏。
- 第三方库,不了解的业务逻辑,都会对排查问题产生阻碍。
- 内存快照的不同类型元素的数量往往很多,都是上万级别的,有工具也不好筛查。
通过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,分析全局变量和闭包。