Skip to content

Node.js 应用程序扩展 (Scaling Application)

尽管 Node.js 的单线程、事件驱动模型在 I/O 并发方面表现出色,但要扩展应用程序以充分利用多核处理器,还需要额外的策略。Node.js 提供内置模块来管理多个进程和线程,从而实现真正的并行性 (parallelism)。

  • 集群 (Clustering):运行 Node.js 应用程序的多个实例(工作进程 worker),这些实例共享同一个服务器端口。内置的 cluster 模块有助于在工作进程之间分配传入连接。
  • 子进程 (Child Processes):使用 child_process 模块生成独立的 Node.js 进程。这对于分载特定任务或运行外部命令非常有用。
  • 工作线程 (Worker Threads):使用 worker_threads 模块(在 Node.js v10.5.0 中引入)在同一进程内并行运行 JavaScript 代码,比子进程更高效地共享内存。非常适合 CPU 密集型 (CPU-bound) 任务。
  • 外部工具:使用 PM2 等进程管理器或 systemd 等系统工具来管理多个实例和重启。
  • 负载均衡器 (Load Balancers):将流量分配到运行 Node.js 应用程序的多个服务器实例上(通常与集群或进程管理器结合使用)。

本章重点介绍 child_process 模块。cluster 和 worker_threads 模块提供了其他方法,它们通常分别更适合扩展 Web 服务器和处理 CPU 密集型工作。

child_process 模块允许你生成新的进程。这些子进程独立于主 Node.js 进程运行,并通过标准输入/输出流 (stdin、stdout、stderr) 和可选的进程间通信 (IPC) 通道与父进程通信。

child_process 中的主要方法:

  • exec():生成一个 shell(如 bash 或 cmd.exe),并在该 shell 中执行命令字符串。在将整个输出返回给回调之前,会将其缓冲起来 (buffer)。对于简单命令很方便,但对于大量输出可能效率不高。
  • spawn():使用指定的命令和参数数组直接生成一个新的进程。它以流 (stream) 的形式提供对 stdin、stdout 和 stderr 的访问,使其适用于处理大量数据或长时间运行的进程。
  • fork():是 spawn() 的一个特例,专门用于创建新的 Node.js 进程。它在父子进程之间建立一个 IPC 通道,允许它们使用 .send() 和 .on('message') 来回发送消息。

child_process.exec(command[, options], callback) 在 shell 中运行命令并缓冲输出。

Signature:
child_process.exec(command[, options], callback)
  • command (String):要运行的命令,包括用空格分隔的参数。
  • options (Object, Optional):配置选项,例如 cwd(当前工作目录)、env(环境变量)、timeout、maxBuffer(stdout/stderr 缓冲区的最大大小,默认为 1MB)、killSignal、uid、gid。
  • callback (Function):当进程终止或遇到错误时调用。接收 (error, stdout, stderr) 参数。如果命令失败,error 是一个 Error 对象;stdout 是标准输出缓冲区,stderr 是标准错误缓冲区。

当你需要一次性获取全部输出且输出大小可控时,使用 exec。

我们创建两个文件:supportScript.js(子脚本)和 masterExec.js(父脚本)。

文件:supportScript.js

const processId = process.argv[2] || 'Unknown';
console.log(`Child Process ${processId} executed.`);
// 模拟一些工作
setTimeout(() => {
console.error(`Child Process ${processId} finishing.`);
}, 500);

文件:masterExec.js

const { exec } = require('child_process');
console.log('Master process starting...');
for (let i = 0; i < 3; i++) {
// 注意:对命令字符串中的用户输入要谨慎,存在 shell 注入风险。
// 考虑使用 spawn 或严格验证/转义输入。
const command = `node supportScript.js ${i}`;
console.log(`正在执行:${command}`);
const child = exec(command, (error, stdout, stderr) => {
console.log(`--- 子进程 ${i} 的回调 ---`);
if (error) {
console.error(`子进程 ${i} 的 exec 错误:`, error);
return;
}
if (stdout) {
console.log(`子进程 ${i} 标准输出:\n${stdout}`);
}
if (stderr) {
console.error(`子进程 ${i} 标准错误:\n${stderr}`);
}
});
child.on('exit', (code, signal) => {
console.log(`子进程 ${i} 已退出,退出码 ${code},信号 ${signal}`);
});
}
console.log('主进程完成子进程启动。');

运行主脚本:

$ node masterExec.js

预期输出(由于异步性,顺序可能有所不同):

Master process starting...
Executing: node supportScript.js 0
Executing: node supportScript.js 1
Executing: node supportScript.js 2
Master process finished initiating children.
--- Callback for Child X ---
Child X stdout:
Child Process X executed.
Child X stderr:
Child Process X finishing.
Child process X exited with code 0 and signal null
(Repeats similarly for other children)

child_process.spawn(command[, args][, options]) 直接启动一个新进程,不经过中间 shell。它提供了对 I/O 流的访问。

Signature:
child_process.spawn(command[, args][, options])
  • command (String):要执行的命令(例如,‘node’、‘ls’、‘python’)。
  • args (Array, Optional):命令的参数字符串列表(例如,[‘script.js’, ‘arg1’])。
  • options (Object, Optional):类似于 exec 的选项,外加 stdio(配置子进程的 stdin/stdout/stderr - 例如,‘pipe’、‘inherit’)、detached(独立于父进程运行)。

spawn 返回一个 ChildProcess 对象,包含 stdin、stdout 和 stderr 流。当处理大量输出、流式传输数据,或需要对进程有更精细控制且不需要 shell 特性时,请使用 spawn。

使用相同的 supportScript.js。创建 masterSpawn.js:

文件:masterSpawn.js

const { spawn } = require('child_process');
console.log('Master process starting...');
for (let i = 0; i < 3; i++) {
const args = ['supportScript.js', i.toString()];
console.log(`正在生成:node ${args.join(' ')}`);
// 生成子进程
const workerProcess = spawn('node', args);
// 监听 stdout 流上的数据
workerProcess.stdout.on('data', (data) => {
console.log(`Child ${i} stdout: ${data.toString().trim()}`);
});
// 监听 stderr 流上的数据
workerProcess.stderr.on('data', (data) => {
console.error(`Child ${i} stderr: ${data.toString().trim()}`);
});
// 监听 'close' 事件(当流关闭时)
workerProcess.on('close', (code) => {
console.log(`Child process ${i} exited with code ${code}`);
});
// 监听潜在的生成错误
workerProcess.on('error', (err) => {
console.error(`Failed to start child process ${i}:`, err);
});
}
console.log('主进程完成子进程启动。');

运行主脚本:

$ node masterSpawn.js

预期输出(顺序可能有所不同,stdout/stderr 会在数据到达时显示):

Master process starting...
Spawning: node supportScript.js 0
Spawning: node supportScript.js 1
Spawning: node supportScript.js 2
Master process finished initiating children.
Child X stdout: Child Process X executed.
Child Y stdout: Child Process Y executed.
Child Z stdout: Child Process Z executed.
Child X stderr: Child Process X finishing.
Child Y stderr: Child Process Y finishing.
Child Z stderr: Child Process Z finishing.
Child process X exited with code 0
Child process Y exited with code 0
Child process Z exited with code 0

child_process.fork(modulePath[, args][, options]) 专门用于生成新的 Node.js 进程。它会启用一个 IPC 通道用于消息传递。

Signature:
child_process.fork(modulePath[, args][, options])
  • modulePath (String):要在子进程中运行的 Node.js 模块的路径。
  • args (Array, Optional):子脚本的字符串参数。
  • options (Object, Optional):类似于 spawn 的选项,外加 execPath(Node.js 可执行文件路径)、execArgv(子进程的 Node.js 运行时参数)、silent(管道子进程的 stdio,而不是继承)。

fork 返回一个 ChildProcess 对象,该对象有一个 .send(message) 方法,并发出 message 事件用于通信。

创建 supportFork.js(子脚本)和 masterFork.js(父脚本)。

文件:supportFork.js

const processId = process.argv[2] || 'Unknown';
console.log(`Child Process ${processId} started.`);
// 监听来自父进程的消息
process.on('message', (msg) => {
console.log(`Child ${processId} received message:`, msg);
// 发送回复回父进程
process.send({ reply: `Roger that from child ${processId}!` });
});
// 模拟一些工作并退出
setTimeout(() => {
console.log(`Child Process ${processId} exiting.`);
process.exit(0);
}, 1500);

文件:masterFork.js

const { fork } = require('child_process');
console.log('Master process starting...');
for (let i = 0; i < 2; i++) { // 生成两个子进程
const args = [i.toString()];
console.log(`正在生成 supportFork.js,参数为:${args}`);
const worker_process = fork('supportFork.js', args);
// 向子进程发送消息
worker_process.send({ message: `Hello from master to child ${i}` });
// 监听来自子进程的消息
worker_process.on('message', (msg) => {
console.log(`Master received message from child ${i}:`, msg);
});
worker_process.on('close', (code) => {
console.log(`Child process ${i} exited with code ${code}`);
});
worker_process.on('error', (err) => {
console.error(`Failed to fork child process ${i}:`, err);
});
}
console.log('主进程完成子进程启动。');

运行主脚本:

$ node masterFork.js

预期输出(顺序/时间可能有所不同):

Master process starting...
Forking supportFork.js with args: 0
Forking supportFork.js with args: 1
Master process finished initiating children.
Child Process 0 started.
Child Process 1 started.
Child 0 received message: { message: 'Hello from master to child 0' }
Master received message from child 0: { reply: 'Roger that from child 0!' }
Child 1 received message: { message: 'Hello from master to child 1' }
Master received message from child 1: { reply: 'Roger that from child 1!' }
Child Process 0 exiting.
Child process 0 exited with code 0
Child Process 1 exiting.
Child process 1 exited with code 1
  • exec:简单的 shell 命令,少量输出。
  • spawn:精细控制,I/O 流式传输,大量数据,不需要 shell 特性。
  • fork:生成带有内置 IPC 的 Node.js 进程。
  • worker_threads:同一进程内的 CPU 密集型任务,共享内存(请谨慎使用)。