Nodejs一次CPU占用飙升问题分析

背景

一台服务器用pm2管理的多个服务,在流量聚集的时候偶现 cpu占用过高 导致响应超时。由于是接手维护的项目,对项目里面所有的调用不是很熟悉,根据当时的日志排查了几个接口没找到原因。

本文主要是记录一下处理步骤以及中间遇到的一些问题。

结论

先说结论:由于部分阻塞计算,在高并发时的接口拥堵导致 cpu占用 飙升;

对异步编程来说,优点是高并发,但这个前提是非阻塞,当系统中有复杂计算(e.g. 业务系统处理大量计算,上传文件等),高并发时很容易出问题。

对有需要复杂计算的情况,除了基本逻辑的处理,需要考虑:

1、多开几个服务(e.g. pm2的cluster模式);

2、通过另外的 进程/线程 来处理复杂逻辑,不阻塞主进程;

问题排查

1、根据当时时间段分析日志,将一些可能导致cpu占用的接口在测试环境进行并发测试;

这个事情很费时间,虽然能分析解决了一些接口有的性能问题,但我们在测试环境没有环境模拟出来这种高并发的情况,主要问题还在;

2、我们的cpu占用表现是瞬间飙升,然后很快降下来,但在高并发的时候出现过两三次一直降不下来的情况;

阿里云服务器的 cpu资源占用 监控,绝大部分情况下无法监控到cpu资源占用的数据,因为他是1分钟采集一次占用数据的。

我们需要一个更精细的监控工具,暂时是自己写一个简单的监控去查看分析cpu占用情况;

3、多服务器负载,这个是最后方案。

但由于我们的服务有一个重要业务是通过 websocket 通信的,当没有做好 websocket消息分发 的时候进行多台服务器负载,总会有一些消息发送异常的情况。

cpu profile

因为实在没办法从日志分析到是哪个 接口/方法 导致的cpu占用过高。

只能将 cpu profile(用的是v8-profiler-next) 放到线上进行记录,一开始我很担心这样会占用更多资源。但在测试环境测试了几次并发感觉还好(在我当前项目情况下,基本看不出来影响,实际上可能需要根据项目的并发程度考虑)。

1、代码主要逻辑是,每五分钟记录一次,一次记录四分钟 cpu调用过程:

javascript 复制代码
cron.schedule('*/5 * * * *', () => {
  const now = new Date();
  const pad = (n) => (n < 10 ? '0' : '') + n; // 补零函数
  const profileName = `cpu-profile-${pid}-${String(now.getFullYear()).slice(2)}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}`;
  const profileFilePath = path.join(profileDir, `${profileName}.cpuprofile`);
  console.log(`[profile] Starting CPU profiling: ${profileFilePath}`);

  // 启动 CPU Profile 记录
  startProfiling(profileName);
  // 记录 4 分钟
  setTimeout(() => {
    // 停止 CPU Profile 记录
    const profile = stopProfiling(profileName);

    // 保存 profile 数据到文件
    fs.writeFileSync(profileFilePath, JSON.stringify(profile));
    console.log(`[profile] CPU profile saved at: ${profileFilePath}`);
  }, 240000);
});

2、分析工具,我原本打算用chrome的dev-tools进行分析,但这个记录的文件无法解析,所以只能通过 speedscope 进行分析。

其实有好几个地方有问题,我举其中一个例子说明:

这是一个上传文件的的方法,可以看到这个服务阻塞了主进程接近3s的时间,后面堆积的任务就会导致cpu瞬间的飙升:

cpu占用分析工具

javascript 复制代码
const pm2 = require('pm2');
const fs = require('fs');

// 定义阈值
const cpuThreshold = 20;  // CPU 占用超过 50% 时记录日志
const reloadThreshold = 95; // CPU 占用超过 95% 时重启进程

// 日志文件路径
const logFilePath = './cpuUsage.log';

// 启动 PM2 并获取所有进程的 CPU 使用情况
pm2.connect((err) => {
  if (err) {
    console.error('PM2 connection error:', err);
    return;
  }

  // 定时检查 PM2 中所有进程的 CPU 使用情况
  setInterval(() => {
    pm2.list((err, processList) => {
      if (err) {
        console.error('Error getting process list:', err);
        return;
      }

      processList.forEach((process) => {
        const pid = process.pid;
        const cpuUsage = process.monit.cpu;  // 获取进程的 CPU 占用
        const name = process.name;

        // 如果 CPU 占用超过阈值,记录到日志文件中
        if (cpuUsage > cpuThreshold) {
          const logMessage = `${new Date().toISOString()} - PID: ${pid}, Name: ${name}, CPU Usage: ${cpuUsage}%\n`;
          fs.appendFileSync(logFilePath, logMessage);  // 将日志写入文件
          console.log(logMessage);  // 输出到控制台
        }

        // 如果 CPU 占用超过重启阈值,执行重启,考虑
     });
    });
  }, 5000); // 每 10 秒检查一次
});

cpu profile

通过插件 v8-profiler-next 提供的 startProfiling, stopProfiling 接口每五分钟记录一次数据

问题记录

1、v8-profiler-next记录的文件格式无法在dev-tools上进行查看,只能通过 speedscope来进行分析;

相关推荐
橘右溪6 小时前
Node.js核心模块及Api详解
node.js
在下千玦1 天前
#管理Node.js的多个版本
node.js
你的人类朋友1 天前
MQTT协议是用来做什么的?此协议常用的概念有哪些?
javascript·后端·node.js
还是鼠鼠1 天前
Node.js中间件的5个注意事项
javascript·vscode·中间件·node.js·json·express
南通DXZ1 天前
Win7下安装高版本node.js 16.3.0 以及webpack插件的构建
前端·webpack·node.js
你的人类朋友1 天前
浅谈Object.prototype.hasOwnProperty.call(a, b)
javascript·后端·node.js
前端太佬1 天前
暂时性死区(Temporal Dead Zone, TDZ)
前端·javascript·node.js
Mintopia1 天前
Node.js 中 http.createServer API 详解
前端·javascript·node.js
你的人类朋友1 天前
CommonJS模块化规范
javascript·后端·node.js
Mintopia2 天前
Node.js 中 fs.readFile API 的使用详解
前端·javascript·node.js