更多精彩内容,欢迎观看:
如何排查 Electron V8 引发的内存 OOM 问题(中):https://developer.aliyun.com/article/1263249?groupCode=taobaotech
如何用 Memory 和 Performance 工具分析内存泄漏问题
前面提到,我们可以通过编译 8G 堆内存的 Electron 版本来缓解 V8FatalErrorCallback 崩溃问题,但这种解决方案会带来以下几个副作用:
- 享受不了 v8 指针压缩带来的好处,会额外增加至少 40% 的内存开销。
- 自行编译 Electron 版本后,需要定期维护更新升级,存在潜在的风险。
- 无法彻底解决 V8FatalErrorCallback 崩溃问题。
因此,我们还是要透过现象去分析问题发生的本质原因。对于客户端应用使用过程中 v8 堆内存一直持续增长问题,根本原因是因为内存泄漏导致。那有人就会问了,v8 不是会自动帮我们 GC 垃圾回收不再使用的内存吗?为啥还会一直持续增长呢?
我们先来看下内存泄漏的定义:当进程不再需要某些内存时,依然没办法回收这些内存。在 JavaScript 中,造成内存泄漏的主要原因是不再需要的内存数据仍然被其他对象引用着。也就是说,这些内存数据里的对象不再使用但引用计数不为 0 导致无法被 GC,造成内存泄漏。
那要如何定位排查出哪块代码导致的内存泄漏呢?我们可以使借助 chrome devtools 提供的 Memory 和 Performance 工具来分析内存泄漏问题。
▐ 如何用 Memory 工具分析内存泄漏问题
我们首先可以通过 Memory 工具,从内存中对象的角度来分析内存泄漏。
首先点击 chrome Memory 面板下的垃圾回收按钮,手动触发一次 GC:
然后录制一次内存快照,过一段时间(如 1min)后再录制一次内存快照。通过这两次内存快照可以得知内存增长了 19.2M,存在内存泄漏的问题。
继续对比 diff 这两次内存快照,发现最大的内存增长是 string 对象。但点 string tab 展开后并不能直接定位到是哪块代码逻辑导致,只能去排查到底哪里在频繁使用string 对象导致的内存泄漏,定位问题不够直观。
▐ 如何用 Performance 工具分析内存泄漏问题
因此,我们还需要通过 Performance 工具,从代码的角度来分析内存泄漏。
首先点击 chrome Performance 面板下的垃圾回收按钮,手动触发一次 GC:
然后勾选 chrome Performance 面板下的 Memory 选项,点击录制按钮开始录制,等录制一段时间(如 1 分钟)后停止录制:
接着查看 chrome Performance 面板下的内存部分,只勾选 JS Heap 看下内存是否有增长趋势。下图显示内存从 24.1M 一直增长到 38.1M,说明存在内存泄漏。
紧接着点击内存分配情况的某个点,就会定位到 Performance 中的某个任务的代码。
最后点击某个任务代码可以定位到分配内存的代码,分析后发现是一直在触发 electron-log 的 onError 事件,具体的应用代码如下:
import TraceSdk from '@ali/trace-sdk-node'import log from 'electron-log' // arms 实时日志上报平台let trace = TraceSdk()const sendErrorLog = trace.logError log.catchErrors({ onError(error) { sendErrorLog(error) },})
通过查看 V8FatalErrorCallback 这类崩溃用户日志发现崩溃前一直在上报 FetchError 的错误信息:
Unhandled Exception FetchError: request to https://s-gm.mmstat.com/arms.1.1 failed, reason: getaddrinfo ENOTFOUND s-gm.mmstat.com at ClientRequest.<anonymous> (http://localhost:2546/home.js:7090:298490) at ClientRequest.emit (node:events:390:28) at TLSSocket.socketErrorListener (node:_http_client:447:9) at TLSSocket.emit (node:events:390:28) at emitErrorNT (node:internal/streams/destroy:157:8) at emitErrorCloseNT (node:internal/streams/destroy:122:3) at processTicksAndRejections (node:internal/process/task_queues:83:21)
另外看下面的调用堆栈猜测可能是主进程被挂起或断网导致 arms 实时日志上报请求会一直失败,然后就会递归触发 electron-log 的 onError 事件:
为了验证这个猜想,我们尝试断网后用 vscode 断点调试发现确实如此,难怪用 Memory 工具对比分析前后一段时间的内存 heapsnapshot 时发现 string 和 array 对象一直在增长,是因为 error 里持有了错误信息的字符串。
既然定位到原因了,那解决起来也很简单,就是把 arms fetchError 这类错误日志过滤掉。不仅可以解决递归 onError 事件导致的内存泄漏问题,还可以过滤由于 arms 本身带来的错误日志。
import TraceSdk from '@ali/trace-sdk-node'import log from 'electron-log' // arms 实时日志上报平台let trace = TraceSdk()const sendErrorLog = trace.logError log.catchErrors({ onError(error) { // 过滤 arms fetch error 错误日志 if (!error?.message.includes('https://s-gm.mmstat.com/arms')) { sendErrorLog(error) } },})
排查这么久,V8FatalErrorCallback js heap OOM 崩溃问题终于破案了。工欲善其事必先利其器,不得不说 chrome devtools 的 Memory 和 Performance 工具对分析内存泄漏问题太有帮助了。
如何监控 v8 堆内存泄漏问题
上一章节我们通过借助 chrome devtools 的 Memory 和 Performance 工具手动分析了 1 例内存泄漏问题,但可能还存在其他如全局变量、变量被闭包引用、游离的 DOM 元素被变量引用、定时器没清除等隐藏的内存泄漏问题。当客户端应用发布后,我们要如何监控线上 v8 堆内存泄漏问题呢?
▐ 如何监控 v8 实时堆内存使用趋势
首先我们可以通过 v8.getHeapStatistics 接口,每隔一段时间(如 1 分钟)采集一次 v8 的堆内存数据,然后上报到 Medialab平台进行监控,实时统计 v8 堆内存使用趋势,下图可以明显发现 v8 堆内存使用一直在增长,存在内存泄漏问题。
▐ 如何监控 v8 堆内存泄漏问题
除了用 v8.getHeapStatistics 接口实时监控 v8 堆内存使用趋势外,我们还可以用 node-memwatch npm 包查找代码中的内存泄漏问题。
node-memwatch 可以监听两个事件:
stats:GC 事件,每执行一次 GC 都会触发该函数并打印 heap 相关的信息,如下所示:
{ num_full_gc: 1,// 完整的垃圾回收次数 num_inc_gc: 1,// 增长的垃圾回收次数 heap_compactions: 1,// 内存压缩次数 usage_trend: 0,// 使用趋势 estimated_base: 5350136,// 预期基数 current_base: 5350136,// 当前基数 min: 0,// 最小值 max: 0// 最大值}
leak:内存泄露事件,触发该事件的条件是:连续 5 次 GC 后内存都是增长的,如下所示:
{ growth: 4051464, reason: 'heap growth over 5 consecutive GCs (2s) - -2147483648 bytes/hr'}
当监听到 leak 内存泄漏事件时,我们可以通过 HeapDiff 来对比分析前后内存快照,以此排查具体是哪个对象发生了内存泄漏。
{ "before": { "nodes": 11625, "size_bytes": 1869904, "size": "1.78 mb" }, "after": { "nodes": 21435, "size_bytes": 2119136, "size": "2.02 mb" }, "change": { "size_bytes": 249232, "size": "243.39 kb", "freed_nodes": 197, "allocated_nodes": 10007, "details": [ { "what": "String", "size_bytes": -2120, "size": "-2.07 kb", "+": 3, "-": 62 }, { "what": "Array", "size_bytes": 66687, "size": "65.13 kb", "+": 4, "-": 78 }, { "what": "LeakingClass", "size_bytes": 239952, "size": "234.33 kb", "+": 9998, "-": 0 } ] }}
总结
本文从 Electron V8FatalErrorCallback 崩溃问题的堆栈分析开始讲起,然后通过堆栈信息一步步使用各种解决方案都无功而返后,尝试分析 v8 源码堆内存限制的实现原理,并开始编译关闭指针压缩的 Electron 源码来提升 v8 堆内存上限以此来延缓问题。但由于该方案会额外带来一些副作用被否掉,最终借助 chrome devtools 提供的 Memory 和 Performance 工具一步步排查定位才解决了 Electron v8 引发的内存 OOM 问题。最后我们通过 v8.getHeapStatistics 来监控 v8 堆内存趋势、node-memwatch 来监控 v8 堆内存是否有泄漏,可以触类旁通解决其他内存 OOM 问题。
团队介绍
我们是大淘宝技术淘宝直播前端团队,负责淘系增长非常快的直播业务,业务上升空间非常大。在技术方面,我们在探索直播间互动、游戏互动、数据可视化、音视频播放器、微前端、智能搭建、Web 3D、Electron跨端开发、桌面推流客户端开发、跨 PC/H5/Native 的多端架构等。在这里你有机会通过一行代码为业务创造亿级 GMV 增量,期待优秀的你!