Node.js 应用程序扩展 (Scaling Application)
Node.js - 扩展你的应用 (Scaling)
Section titled “Node.js - 扩展你的应用 (Scaling)”尽管 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 模块
Section titled “child_process 模块”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')来回发送消息。
exec() 方法
Section titled “exec() 方法”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。
示例 (exec)
Section titled “示例 (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 0Executing: node supportScript.js 1Executing: node supportScript.js 2Master 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)spawn() 方法
Section titled “spawn() 方法”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。
示例 (spawn)
Section titled “示例 (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 0Spawning: node supportScript.js 1Spawning: node supportScript.js 2Master 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 0Child process Y exited with code 0Child process Z exited with code 0fork() 方法
Section titled “fork() 方法”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 事件用于通信。
示例 (fork)
Section titled “示例 (fork)”创建 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: 0Forking supportFork.js with args: 1Master 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 0Child Process 1 exiting.Child process 1 exited with code 1选择合适的方法:
Section titled “选择合适的方法:”exec:简单的 shell 命令,少量输出。spawn:精细控制,I/O 流式传输,大量数据,不需要 shell 特性。fork:生成带有内置 IPC 的 Node.js 进程。worker_threads:同一进程内的 CPU 密集型任务,共享内存(请谨慎使用)。