背景
一台服务器用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来进行分析;