如何排查 Electron V8 引发的内存 OOM 问题(下)

本文涉及的产品
应用实时监控服务-应用监控,每月50GB免费额度
应用实时监控服务-用户体验监控,每月100OCU免费额度
日志服务 SLS,月写入数据量 50GB 1个月
简介: 如何排查 Electron V8 引发的内存 OOM 问题(下)

更多精彩内容,欢迎观看:

如何排查 Electron V8 引发的内存 OOM 问题(中):https://developer.aliyun.com/article/1263249?groupCode=taobaotech


如何用 Memory 和 Performance 工具分析内存泄漏问题


前面提到,我们可以通过编译 8G 堆内存的 Electron 版本来缓解 V8FatalErrorCallback 崩溃问题,但这种解决方案会带来以下几个副作用:

  1. 享受不了 v8 指针压缩带来的好处,会额外增加至少 40% 的内存开销。
  2. 自行编译 Electron 版本后,需要定期维护更新升级,存在潜在的风险。
  3. 无法彻底解决 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 增量,期待优秀的你!


相关实践学习
通过云拨测对指定服务器进行Ping/DNS监测
本实验将通过云拨测对指定服务器进行Ping/DNS监测,评估网站服务质量和用户体验。
相关文章
|
3月前
|
监控 Java Linux
redisson内存泄漏问题排查
【9月更文挑战第22天】在排查 Redisson 内存泄漏问题时,首先需确认内存泄漏的存在,使用专业工具(如 JProfiler)分析内存使用情况,检查对象实例数量及引用关系。其次,检查 Redisson 使用方式,确保正确释放资源、避免长时间持有引用、检查订阅和监听器。此外,还需检查应用程序其他部分是否存在内存泄漏源或循环引用等问题,并考虑更新 Redisson 到最新版本以修复潜在问题。
102 5
|
4月前
|
JavaScript Java 开发工具
Electron V8排查问题之接近堆内存限制的处理如何解决
Electron V8排查问题之接近堆内存限制的处理如何解决
240 1
|
5月前
|
监控 安全 Java
JVM内存问题之排查Direct Memory泄漏有哪些常用方法
JVM内存问题之排查Direct Memory泄漏有哪些常用方法
138 2
|
4月前
|
搜索推荐 Java API
Electron V8排查问题之分析 node-memwatch 提供的堆内存差异信息来定位内存泄漏对象如何解决
Electron V8排查问题之分析 node-memwatch 提供的堆内存差异信息来定位内存泄漏对象如何解决
119 0
|
4月前
|
Web App开发 监控 Java
Electron V8排查问题之发现的内存泄漏问题如何解决
Electron V8排查问题之发现的内存泄漏问题如何解决
178 0
|
4月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
394 0
|
2月前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
64 1
|
2月前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
2月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
2月前
|
存储 编译器
数据在内存中的存储
数据在内存中的存储
42 4