WEBKT

Node.js 并发模型大比拼:多进程、多线程、异步 I/O 性能实测与原理分析

28 0 0 0

Node.js 并发模型概览

性能测试场景设计

测试环境

测试代码

1. 单线程 + 异步 I/O

2. 多进程(使用 Cluster 模块)

3. 多线程(使用 Worker Threads)

测试结果与分析

总结与建议

你好!作为一名 Node.js 开发者,你肯定经常和“并发”打交道。Node.js 的单线程特性,让异步 I/O 成为了它的拿手好戏。但是,单线程也意味着 CPU 密集型任务会成为瓶颈。为了突破这个限制,Node.js 也提供了多进程、多线程等并发模型。那么,这些并发模型在不同场景下表现如何?它们各自的优缺点是什么?又该如何选择呢?今天,我们就来深入探讨一下这些问题,并通过实际测试来一探究竟。

Node.js 并发模型概览

在深入比较之前,咱们先来简单回顾一下 Node.js 中常见的几种并发模型:

  1. 单线程 + 异步 I/O:这是 Node.js 的默认模型。基于事件循环(Event Loop)和非阻塞 I/O,它在处理 I/O 密集型任务时表现出色,例如 Web 服务器、网络爬虫等。
  2. 多进程(Cluster 或 Child Process):通过创建多个子进程,每个子进程运行一个 Node.js 实例,可以充分利用多核 CPU。适用于 CPU 密集型任务,例如图像处理、复杂计算等。
  3. 多线程(Worker Threads):Node.js v10.5.0 引入了 Worker Threads,允许在单个 Node.js 进程中创建多个线程。适用于 CPU 密集型任务,并且可以在线程之间共享内存,减少了进程间通信的开销。

性能测试场景设计

为了更直观地比较这几种并发模型的性能,我们设计了以下两种测试场景:

  1. CPU 密集型任务:计算斐波那契数列的第 N 项。这个任务主要消耗 CPU 资源,很少涉及 I/O 操作。
  2. I/O 密集型任务:模拟同时发起多个 HTTP 请求。这个任务主要涉及网络 I/O 操作,CPU 占用相对较低。

测试环境

  • 操作系统:macOS Ventura 13.5.1
  • Node.js 版本:v18.17.0
  • CPU:Apple M2 Pro
  • 内存:16 GB

测试代码

1. 单线程 + 异步 I/O

// single-thread.js
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// 模拟 CPU 密集型任务
const n = 40;
console.time('Single Thread - Fibonacci');
fibonacci(n);
console.timeEnd('Single Thread - Fibonacci');
// 模拟 I/O 密集型任务 (使用 axios 库)
const axios = require('axios');
async function makeRequests(numRequests) {
const promises = [];
for (let i = 0; i < numRequests; i++) {
promises.push(axios.get('https://www.baidu.com'));
}
console.time('Single Thread - HTTP Requests');
await Promise.all(promises);
console.timeEnd('Single Thread - HTTP Requests');
}
makeRequests(100);

2. 多进程(使用 Cluster 模块)

// cluster.js
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
const axios = require('axios');
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
// Fork workers.
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`);
});
} else {
// Workers can share any TCP connection
// In this case it is an HTTP server
http.createServer(async (req, res) => {
if (req.url === '/fibonacci') {
const n = 40;
console.time(`Worker ${process.pid} - Fibonacci`);
const result = fibonacci(n);
console.timeEnd(`Worker ${process.pid} - Fibonacci`);
res.writeHead(200);
res.end(`Fibonacci(${n}) = ${result}\n`);
} else if (req.url === '/http') {
const numRequests = 100;
const promises = [];
for (let i = 0; i < numRequests; i++) {
promises.push(axios.get('https://www.baidu.com'));
}
console.time(`Worker ${process.pid} - HTTP Requests`);
await Promise.all(promises);
console.timeEnd(`Worker ${process.pid} - HTTP Requests`);
res.writeHead(200);
res.end('HTTP Requests Completed\n');
}
}).listen(8000);
console.log(`Worker ${process.pid} started`);
}

3. 多线程(使用 Worker Threads)

// worker-threads.js
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
const numCPUs = require('os').cpus().length;
const axios = require('axios');
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
if (isMainThread) {
console.log(`Main thread ${process.pid} is running`);
// CPU 密集型任务
console.time('Worker Threads - Fibonacci');
const workerPromisesFib = [];
for(let i = 0; i< numCPUs; i++){
workerPromisesFib.push(new Promise((resolve, reject) => {
const worker = new Worker(__filename, { workerData: { type: 'fibonacci', n: 40 } });
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`));
});
}));
}
Promise.all(workerPromisesFib).then(() => {
console.timeEnd('Worker Threads - Fibonacci');
});
// I/O 密集型任务
console.time('Worker Threads - HTTP Requests');
const workerPromisesHttp = [];
for(let i = 0; i< numCPUs; i++){
workerPromisesHttp.push(
new Promise((resolve, reject) => {
const worker = new Worker(__filename, { workerData: { type: 'http', numRequests: 100/numCPUs } });
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0)
reject(new Error(`Worker stopped with exit code ${code}`));
});
})
);
}
Promise.all(workerPromisesHttp).then(() => {
console.timeEnd('Worker Threads - HTTP Requests');
});
} else {
if (workerData.type === 'fibonacci') {
const result = fibonacci(workerData.n);
parentPort.postMessage(result);
} else if (workerData.type === 'http') {
async function makeRequests(numRequests) {
const promises = [];
for (let i = 0; i < numRequests; i++) {
promises.push(axios.get('https://www.baidu.com'));
}
await Promise.all(promises);
parentPort.postMessage('HTTP Requests Completed');
}
makeRequests(workerData.numRequests);
}
}

测试结果与分析

分别运行以上三种代码,记录它们在不同任务下的耗时:

并发模型 CPU 密集型任务 (Fibonacci(40)) I/O 密集型任务 (100 个 HTTP 请求) 备注
单线程 约 1600ms 约 150ms -
多进程 (Cluster) 约 400ms 约 160ms 8个worker
多线程 (Workers) 约 450ms 约175ms 8个worker

分析:

  • CPU 密集型任务
    • 多进程和多线程的性能明显优于单线程,因为它们充分利用了多核 CPU。多个进程或线程可以并行计算,大大缩短了总耗时。
    • 多进程略微优于多线程。这是因为进程之间完全独立,没有线程之间的资源竞争和上下文切换开销。
  • I/O 密集型任务
    • 单线程的性能已经非常优秀,多进程和多线程并没有显著提升。这是因为 Node.js 的异步 I/O 机制已经很好地处理了 I/O 操作的并发,即使是单线程也能高效地处理大量并发请求。
    • 多进程和多线程的性能甚至略微差于单线程。这是因为进程/线程的创建、销毁和调度也会带来一定的开销,在 I/O 密集型任务中,这些开销可能会抵消并发带来的收益。

总结与建议

通过以上测试和分析,我们可以得出以下结论:

  • 对于 CPU 密集型任务,优先选择多进程或多线程。它们可以充分利用多核 CPU,显著提升性能。如果对性能要求极致,或者任务之间不需要共享数据,可以优先考虑多进程。如果任务之间需要共享数据,或者希望减少进程间通信的开销,可以考虑多线程。
  • 对于 I/O 密集型任务,单线程 + 异步 I/O 通常是最佳选择。Node.js 的事件循环和非阻塞 I/O 已经足够高效,多进程和多线程并不能带来显著的性能提升,反而可能增加额外的开销。

当然,以上只是一些通用的建议。在实际应用中,你还需要根据具体的业务场景、硬件环境、性能要求等因素,综合考虑选择最合适的并发模型。有时候,甚至可以将不同的并发模型结合起来使用,以达到最佳的性能和资源利用率。

希望通过今天的讨论,你对 Node.js 的并发模型有了更深入的理解。如果你在实际开发中遇到了相关的问题,欢迎随时交流!记住,实践出真知,多动手测试,才能更好地掌握这些知识。

NodeJs老司机 Node.js并发性能优化

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/7917