• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

electron资源cpu/memory占用过高的处理思路

武飞扬头像
vb
帮助1

背景

最近发现客户端(Electron IM)在某些场景下 CPU/Memory 占用较高,所以需要进一步分析定位问题。本文主要记录这次资源占用优化的过程, 以及尝试分析优化效果。

备注:本文主要是介绍Electron 主进程的资源占用优化,监控资源使用的折线图将cpu/memory放一起了。折线图用plotly生成。关于数据收集可以查看我之前的一篇文章electron性能优化系列一(进程管理)

结论

先说结论,在这次优化过程中,其实大部分时间在思考和测试量化优化结果这个事情上。 分两阶段说明:
1、提高性能,提高TPS;
2、控制并发数量,避免高资源占用;

一开始我的评估指标是在同样的QPS上,资源占用的对比。 e.g.
1、在 200QPS 的情况下,未优化前的资源占用情况:
学新通

2、在经过第一阶段(主要是数据库查询性能)优化后, 200QPS 的资源占用情况:
学新通 可以看到cpu占用降低了一些,执行时间更短,但仍然会有资源占用的飙升。

其实从两个结果对比,很难去量化到底优化了多少,同时仍会存在高并发资源占用飙升的情况。
花了一些时间去思考这个问题,其实在足够大的并发情况下,不管如何优化都会出现高资源的占用(在非常高并发的情况下,我第一阶段的优化只是提高TPS,解决不了高资源占用的问题)。所以对高资源占用的优化思路应该是降低/控制并发数量。e.g. 延迟队列,线程池/进程池限制并发数等。其实对控制并发数量和TPS中取得平衡也是一件比较有意思的事情(后面有场景再写一篇文章说明)。
同时因为在我目前的场景中存在大量幂等的调用,所以通过节流可以有效解决:
学新通

第一阶段,提高TPS

首先需要一个工具来测量TPS,因为不像平时的后端开发,可以借助测试工具进行请求返回,目前也找不到类似的工具,所以自己实现了一个简单的TPS统计方法:

// 主要功能是在第一个开启事务的地方标识时间
// 最后完成所有事务后统计TPS
class TPSCalculator {
    constructor() {
      this.transactions = new Map(); // 用于存储事务的开始时间
      this.completedTransactions = new Set(); // 用于存储已完成的事务
      this.startTime = null; // 记录开始时间
    }
  
    static getInstance() {
      if (!TPSCalculator.instance) {
        TPSCalculator.instance = new TPSCalculator();
      }
      return TPSCalculator.instance;
    }
  
    startTransaction(key) {
      if (this.transactions.size === 0) {
        this.startTime = Date.now(); // 第一个任务开始时记录开始时间
      }
      const startTime = Date.now();
      this.transactions.set(key, startTime);
    }
  
    endTransaction(key) {
      if (this.transactions.has(key)) {
        const startTime = this.transactions.get(key);
        const endTime = Date.now();
        const transactionTime = endTime - startTime;
        console.log(`Transaction '${key}' took ${transactionTime}ms`);
  
        this.transactions.delete(key);
        this.completedTransactions.add(key);
  
        if (this.transactions.size === 0) {
          this.calculateTPS();
        }
      } else {
        console.log(`Transaction '${key}' not found`);
      }
    }
  
    calculateTPS() {
      const endTime = Date.now();
      const totalTime = endTime - this.startTime;
      const transactions = this.completedTransactions.size;
      const tps = (transactions / totalTime) * 1000; // 计算 TPS,乘以 1000 转换为每秒
  
      console.log('Current TPS:', tps);
    }
}

测试结果在目前 200QPS 的情况下,只有1.2左右的TPS.

定位耗时计算

将Electron通过调试模式(--inspect-brk)启动, 记录并发时的cpu调用过程:
学新通 从图可以看到主要是一个 query 的方法占用了cpu资源,这个是typeORM框架的sql查询方法。
但这里没办法定位到是哪个sql语句导致,需要用到typeORM的另外一个工具,将超过50ms的慢查询打印出来:
学新通 可以看到在高并发的时候出现了大量的慢查询(100-350ms)。

SQL优化

我主要是进行以下几步的优化(在步骤下有粗略的统计优化效果,跟很多因素有关,e.g. 数据量/环境等,只能在同等环境下做一个对比):
1、索引优化(关于sqlite索引优化的更多内容可以查看我之前的文章: sqlite索引优化);

// 200QPS的
// 优化前: 1.2TPS
// 优化后: 1.4TPS

2、减少不必要的查询(这部分主要跟业务逻辑相关,这里不再赘述),限制查询数量(有些sql其实不需要查询全表数据,限制查询的数量,提高sql性能);

// 优化前: 1.4TPS
// 优化后: 6.7TPS

(之所以第二步优化效果比较明显是因为我这里查的是消息表,虽然已经分表,但每个表仍有几万的数据,限制查询数量可以有很高的性能提升。)
经过这两步SQL优化后:
学新通 可以看到cpu的调用已经健康了很多,当在高并发的情况下,仍会出现短时间内资源占用过高的问题。

另:其实在我的使用场景下,这里的场景下使用缓存也能非常高效的提高性能,但因为该数据的插入/更新逻辑太多,暂时没有进行缓存(sqlite目前已支持 RETURNING 语法,后面类似的项目在设计的时候应该需要考虑这点)。

第二阶段优化

经过第一阶段的优化后,其实性能已经提升了一大截,但资源占用过高问题仍存在:
理论上,当并发数量设置的非常高时,资源占用问题是一定存在的(在不同的场景可能不太一样,例如我上面这个优化,如果使用缓存,TPS应该可以很高,后面有时间去重构后再补充这部分数据)。
这种时候应该用另外一种处理思路,限制并发数(以时间换空间),限制并发的处理思路很多,e.g.
1、前端限制;
2、延迟队列;
3、线程池/进程池;
4、防抖节流;
...
因为在我的使用场景下该任务中比较消耗资源的都是幂等的调用,所以我是通过防抖/节流来进行调用的。节流后的效果如下:
学新通 (其实节流也有个注意的地方,例如我测出来的TPS目前是6.7TPS左右,大概就是150ms处理一个任务,考虑到不同的客户端的性能差异,我设置的是300ms)

Memory

其实主进程在处理完高cpu占用后,内存占用也跟着下去了。但这次对内存占用的进一步了解,给我提供了一些比较清晰的思路反馈到开发过程中,但内容较多,下一篇文章再写一下。

问题记录

1、如何根据参数进行防抖/节流?
有点麻烦,需要针对不同的参数创建自己的防抖函数,e.g.

const _ = require('lodash')
// 根据唯一key进行防抖
// 需要存储节流函数
const debounceMap = {}
function argumentDebounce(key, fn) {
    if(!debounceMap[key]) {
        debounceMap[key] = _.throttle(fn, 300, {leading: true, trailing: true})
    }

    return debounceMap[key]
}

2、nodejs中是否需要手动触发内存回收,以及如何手动触发内存回收?
通过 --expose-gc 参数启动应用,在需要进行手动触发的地方执行 global.gc() 即可,但在electron中有点特殊, 关联issue.

参考文档

1、RETURNING: www.sqlite.org/lang_return…
2、Avoiding Memory Leaks in Node.js: Best Practices for Performance: blog.appsignal.com/2020/05/06/…

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhebhkka
系列文章
更多 icon
同类精品
更多 icon
继续加载