Node.js 多进程
cluster 榨干多核、child_process 调外部命令、worker_threads 做计算。
Key PointsWeb 服务用 `cluster` 复用端口,外部命令用 `child_process`,纯计算用 `worker_threads`。
三种并发手段
| Tool | Isolation | Best for |
|---|---|---|
cluster | 独立进程、独立内存 | HTTP 服务多核 |
child_process | 独立进程 | 调用 ffmpeg、git、python |
worker_threads | 独立线程、共享内存可选 | 图像、加密、大 JSON |
cluster
JavaScript
const cluster = require('node:cluster');
const http = require('node:http');
const os = require('node:os');
if (cluster.isPrimary) {
const n = os.cpus().length;
console.log(`主进程 ${process.pid} 启动 ${n} 个 worker`);
for (let i = 0; i < n; i++) cluster.fork();
cluster.on('exit', (worker, code) => {
console.log(`worker ${worker.process.pid} 退出(${code}),重启`);
cluster.fork();
});
} else {
http.createServer((req, res) => {
res.end(`由 ${process.pid} 处理\n`);
}).listen(3000);
console.log(`worker ${process.pid} 就绪`);
}Output
主进程 24188 启动 8 个 worker
worker 24190 就绪
worker 24191 就绪
...
$ curl http://127.0.0.1:3000
由 24190 处理
$ curl http://127.0.0.1:3000
由 24191 处理Multiple workers listening on one port are balanced by the primary (round-robin outside Windows). Re-fork in exit is what keeps the service alive.
child_process
JavaScript
const { exec, spawn } = require('node:child_process');
const { promisify } = require('node:util');
const execP = promisify(exec);
// 短命令:拿全部输出
const { stdout } = await execP('git rev-parse --short HEAD');
console.log('commit', stdout.trim());
// 长任务:流式读
const ff = spawn('ffmpeg', ['-i', 'in.mp4', 'out.mp3']);
ff.stderr.on('data', (d) => process.stderr.write(d));
ff.on('close', (code) => console.log('退出码', code));exec hands the string to a shell, so interpolating user input is command injection. Pass arguments as an array with execFile or spawn.worker_threads
JavaScript
// main.js
const { Worker } = require('node:worker_threads');
function runTask(n) {
return new Promise((resolve, reject) => {
const w = new Worker('./worker.js', { workerData: { n } });
w.on('message', resolve);
w.on('error', reject);
});
}
const results = await Promise.all([runTask(40), runTask(41), runTask(42)]);
console.log(results);
// worker.js
const { parentPort, workerData } = require('node:worker_threads');
function fib(n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
parentPort.postMessage(fib(workerData.n));Output
[ 102334155, 165580141, 267914296 ]The same work on the main thread freezes the loop for seconds; in a worker the main thread keeps serving. This is the right pattern for CPU-bound work in Node.
该选哪个
- 要响应 HTTP 请求 →
cluster(或直接用 PM2 的 cluster 模式) - 要跑一个外部程序 →
child_process - 要算一个耗时算法 →
worker_threads
三种并发手段
| 工具 | 隔离性 | 适用 |
|---|---|---|
cluster | 独立进程、独立内存 | HTTP 服务多核 |
child_process | 独立进程 | 调用 ffmpeg、git、python |
worker_threads | 独立线程、共享内存可选 | 图像、加密、大 JSON |
cluster
JavaScript
const cluster = require('node:cluster');
const http = require('node:http');
const os = require('node:os');
if (cluster.isPrimary) {
const n = os.cpus().length;
console.log(`主进程 ${process.pid} 启动 ${n} 个 worker`);
for (let i = 0; i < n; i++) cluster.fork();
cluster.on('exit', (worker, code) => {
console.log(`worker ${worker.process.pid} 退出(${code}),重启`);
cluster.fork();
});
} else {
http.createServer((req, res) => {
res.end(`由 ${process.pid} 处理\n`);
}).listen(3000);
console.log(`worker ${process.pid} 就绪`);
}Output
主进程 24188 启动 8 个 worker
worker 24190 就绪
worker 24191 就绪
...
$ curl http://127.0.0.1:3000
由 24190 处理
$ curl http://127.0.0.1:3000
由 24191 处理多个 worker 监听同一端口由主进程做负载均衡(除 Windows 外是轮询)。exit 里重新 fork 是保活的关键。
child_process
JavaScript
const { exec, spawn } = require('node:child_process');
const { promisify } = require('node:util');
const execP = promisify(exec);
// 短命令:拿全部输出
const { stdout } = await execP('git rev-parse --short HEAD');
console.log('commit', stdout.trim());
// 长任务:流式读
const ff = spawn('ffmpeg', ['-i', 'in.mp4', 'out.mp3']);
ff.stderr.on('data', (d) => process.stderr.write(d));
ff.on('close', (code) => console.log('退出码', code));exec 会把命令交给 shell 解析,用户输入拼进命令就是命令注入漏洞。要传参数用 execFile 或 spawn,把参数放在数组里。worker_threads
JavaScript
// main.js
const { Worker } = require('node:worker_threads');
function runTask(n) {
return new Promise((resolve, reject) => {
const w = new Worker('./worker.js', { workerData: { n } });
w.on('message', resolve);
w.on('error', reject);
});
}
const results = await Promise.all([runTask(40), runTask(41), runTask(42)]);
console.log(results);
// worker.js
const { parentPort, workerData } = require('node:worker_threads');
function fib(n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
parentPort.postMessage(fib(workerData.n));Output
[ 102334155, 165580141, 267914296 ]同样的计算放在主线程会让事件循环停几秒;放进 worker,主线程照常响应请求。这是 CPU 密集任务在 Node 里的正确姿势。
该选哪个
- 要响应 HTTP 请求 →
cluster(或直接用 PM2 的 cluster 模式) - 要跑一个外部程序 →
child_process - 要算一个耗时算法 →
worker_threads