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来进行分析;

相关推荐
<e^πi+1=0>2 小时前
使用Node编写服务器接口
node.js
vvw&15 小时前
在 Ubuntu 22.04 上部署 AppArmor 应用安全教程
linux·运维·服务器·nginx·安全·ubuntu·node.js
LLLuckyGirl~20 小时前
node.js内置模块之---stream 模块
node.js
【D'accumulation】1 天前
基于 Node.js 的 ORM(对象关系映射)工具——Sequelize介绍与使用,并举案例分析
前端·javascript·学习·node.js·express
南城巷陌1 天前
Node.js中使用Joi 和 express-joi-validation进行数据验证和校验
前端·node.js·express·数据校验
果冻~1 天前
使用 NestJS 构建高效且模块化的 Node.js 应用程序,从安装到第一个 API 端点:一步一步指南
node.js
疯狂的沙粒2 天前
如何在 JavaScript 中实现日期格式化?
开发语言·前端·css·node.js
InnovatorX2 天前
Node.js 知识(规范)
node.js
humors2212 天前
怎样修改el-table主题样式
运维·前端·vue.js·node.js
LLLuckyGirl~2 天前
node.js内置模块之---http 和 https 模块
node.js