当Node.js服务出现性能问题时,可以通过以下的方法进行排查:
基础检查
# 查看监听特定端口进程PID
lsof -i:8080
# 监控进程资源占用情况
top -pid {pid}
# 查看磁盘IO性能状况
# -c 重复显示的次数 -w 每隔多少秒输出一次
iostat -c 2 -w 1
请求延迟高
排查方法
- 启用V8内置的分析器
node --cpu-prof app.js
在进程正常退出后,会当前目录生成 *.cpuprofile 文件。该文件可以通过 Chrome DevTools 查看,也可以通过一些火焰图可视化工具查看。
这里我们选择用 VSCode 查看(需要安装火焰图插件)文件,如下图:
🌟 关注Self Time
- 启用事件循环跟踪
node --trace-event-categories=node.perf,node.async_hooks app.js
运行后会自动生成 node_trace.log 文件,可以用 chrome://tracing 查看。如下图:
🌟 关注Wall Duration
- 使用
clinic性能分析工具
clinic flame -- node app.js
在退出进程后,会自动生成结果文件 *flame.html。通过浏览器打开:
- 使用
process.cupUsage()
const startUsage = process.cpuUsage();
// ...执行关键操作
const endUsage = process.cpuUsage(startUsage);
console.log(`CPU Time: user ${endUsage.user / 1000} system ${endUsage.system / 1000}`);
// 输出结果
// CPU Time: user 49.644 system 0.227
优化方案
根据上述排查结果可知,函数 pbkdf2Sync 执行相当耗时。它是一个同步函数,会阻塞事件循环,导致系统吞吐量下降。为了解决该问题,我们改为使用异步版本的 pbkdf2 函数:
crypto.pbkdf2(password, user.salt, 10000, 512, 'sha512', (err, buf) => {
if (user.password === buf.toString('hex')) {
res.status(200).json({
code: 0,
message: 'Success.',
data: {
user: _.omit(user, ['password', 'salt']),
jwt: 'jwt',
},
});
} else {
res.status(401).json({ message: 'Unauthorized.' });
}
});
进行负载测试:
优化前:
优化后:
可以看到,优化过后,服务并发能力大幅度提高!
内存泄漏
排查方法
- 使用
process.memoryUsage()
function showMem() {
const mem = process.memoryUsage();
function format(bytes) {
return Math.floor(bytes / 1024 / 1024) + 'MB';
}
console.log(`Memory Usage: heapTotal ${format(mem.heapTotal)} | heapUsed ${format(mem.heapUsed)} | rss ${format(mem.rss)}`);
}
showMem();
// ...执行关键操作
输出如下:
可以看到,每次调用都使导致内存增长。
- 使用
heapdump模块
// 在文件头部引入模块
const heapdump = require('heapdump');
在进程启动后,通过向进程发送 SIGUSR2 信号,就会生成一份堆内存的快照。该文件可以通过 Chrome DevTools 查看,如下图:
通过分析可知,有大量的 "leak" 字符串存在,这些字符串就是一直未能得到回收的数据。
- 使用
clinic性能分析工具
clinic heapprofiler -- node app.js
在退出进程后,会自动生成结果文件 *heapprofiler.html。通过浏览器打开:
从上图可知,已经定位到了一次 “最大的内存分配” 以及发生分配的代码行,下方也清晰地显示了调用链路。
负载测试工具
- wrk
# 发送POST请求
wrk -c 20 -t 4 -d 100s data.lua http://localhost:8080
data.lua
wrk.method = "POST"
wrk.body = '{"username":"Aric","password":"12345678"}'
wrk.headers["Content-Type"] = "application/json"
- ab
# 发送POST请求
ab -k -c 20 -n 250 -p data.json -T 'application/json' http://localhost:8080
总结
以上就是在对 Node.js 服务进行本地排查时最常用的手段。在生产环境中,通常需要通过APM工具(New Relic等),结合日志系统进行分析。
优化建议
在使用 Node.js 构建服务时:
- 注意对内存的使用,尽量不要在本地存储大对象,如果确实需要,可以通过一些淘汰机制限制内存使用。更推荐使用专业地内存数据库,如
memcached、redis等。 - 针对CPU密集型任务,可以使用
Worker threads去执行,避免长时间阻塞事件循环。 - 尽量将一些操作异步化,通过回调的形式或者事件通知(借助消息队列组件等)获取结果。
测试代码
高延迟示例:
const express = require('express');
const crypto = require('node:crypto');
const _ = require('lodash');
const { faker } = require('@faker-js/faker');
const salt = crypto.randomBytes(128).toString('base64');
const users = [
{
userId: faker.string.uuid(),
username: 'aric',
email: 'aric.vvang@gmail.com',
userRole: 'ADMIN',
salt,
password: crypto.pbkdf2Sync('12345678', salt, 10000, 512, 'sha512').toString('hex'),
},
];
const app = express();
app.use(express.json());
app.post('/auth', (req, res, next) => {
const {
username, password,
} = req.body;
if (!password) {
res.status(400).json({ message: '"password" is not to be allowed empty.' });
return;
}
const user = users.find((o) => o.username === username)
if (!user) {
res.status(404).json({ message: 'user is not registered.' });
return;
}
const encryptPassword = crypto.pbkdf2Sync(password, user.salt, 10000, 512, 'sha512');
if (!crypto.timingSafeEqual(Buffer.from(user.password, 'hex'), encryptPassword)) {
res.status(401).json({ message: 'Unauthorized.' });
return;
}
res.status(200).json({
code: 0,
message: 'Success.',
data: {
user: _.omit(user, ['password', 'salt']),
jwt: 'jwt',
},
});
});
app.listen(8080, () => console.log('Server running at http://localhost:8080/'));
['SIGINT', 'SIGTERM'].forEach((signal) => process.on(signal, () => process.exit(0)));
内存泄漏示例:
const express = require('express');
const leakArray = [];
const app = express();
app.get('/leak', (req, res, next) => {
leakArray.push(new Array(1024 * 1024).fill('leak'));
res.status(200).json({ message: 'Success' });
});
app.listen(8080, () => console.log('Server running at http://localhost:8080/'));