HoRain云--Node.js 多进程 我们都知道 Node.js 是以单线程的模式运行的但它使用的是事件驱动来处理并发这样有助于我们在多核 cpu 的系统上创建多个子进程从而提高性能。每个子进程总是带有三个流对象child.stdin, child.stdout 和child.stderr。他们可能会共享父进程的 stdio 流或者也可以是独立的被导流的流对象。Node 提供了 child_process 与 cluster 模块来创建子进程。child_process模块exec- child_process.exec 使用子进程执行命令缓存子进程的输出并将子进程的输出以回调函数参数的形式返回。execFile- 直接执行可执行文件略安全。spawn- child_process.spawn 使用指定的命令行参数创建新进程。fork- child_process.fork 是 spawn()的特殊形式用于在子进程中运行的模块如 fork(./son.js) 相当于 spawn(node, [./son.js]) 。与spawn方法不同的是fork会在父进程与子进程之间建立一个通信管道用于进程之间的通信。cluster模块在一个端口上多工作进程共享监听实现一核一进程的 HTTP 服务。适合把 Web 服务横向吃满多核。备注官方更推荐新项目仔细评估如直接用反向代理 多进程、或worker_threads做 CPU 密集但cluster依旧广泛可用。exec() 方法child_process.exec 使用子进程执行命令缓存子进程的输出并将子进程的输出以回调函数参数的形式返回。语法如下所示child_process.exec(command[, options], callback)参数参数说明如下command字符串 将要运行的命令参数使用空格隔开options 对象可以是cwd 字符串子进程的当前工作目录env对象 环境变量键值对encoding 字符串字符编码默认 utf8shell 字符串将要执行命令的 Shell默认: 在 UNIX 中为/bin/sh 在 Windows 中为cmd.exe Shell 应当能识别-c开关在 UNIX 中或/s /c在 Windows 中。 在Windows 中命令行解析应当能兼容cmd.exetimeout数字超时时间默认 0maxBuffer数字 在 stdout 或 stderr 中允许存在的最大缓冲二进制如果超出那么子进程将会被杀死 默认: 200*1024killSignal 字符串结束信号默认SIGTERMuid数字设置用户进程的 IDgid数字设置进程组的 IDcallback 回调函数包含三个参数error, stdout 和 stderr。exec() 方法返回最大的缓冲区并等待进程结束一次性返回缓冲区的内容。实例让我们创建两个 js 文件 support.js 和 master.js。support.js 文件代码console.log(进程 process.argv[2] 执行。 );master.js 文件代码const fs require(fs); const child_process require(child_process); for(var i0; i3; i) { var workerProcess child_process.exec(node support.js i, function (error, stdout, stderr) { if (error) { console.log(error.stack); console.log(Error code: error.code); console.log(Signal received: error.signal); } console.log(stdout: stdout); console.log(stderr: stderr); }); workerProcess.on(exit, function (code) { console.log(子进程已退出退出码 code); }); }执行以上代码输出结果为$ node master.js 子进程已退出退出码 0 stdout: 进程 1 执行。 stderr: 子进程已退出退出码 0 stdout: 进程 0 执行。 stderr: 子进程已退出退出码 0 stdout: 进程 2 执行。 stderr:解析exec会启动3 个子进程分别执行node support.js 0node support.js 1node support.js 2每个子进程运行后立刻输出进程 0 执行。 进程 1 执行。 进程 2 执行。这部分是子进程的标准输出(stdout)会被缓冲等子进程结束时一起传回exec的回调。子进程退出时会先触发exit事件然后exec的回调才会执行注意Node.js 文档里说明exit事件发生在进程结束时而exec的回调是stdio流关闭后触发通常 exit 会先于回调打印。输出顺序是不确定的因为 3 个子进程并行执行谁先结束取决于操作系统调度。spawn() 方法child_process.spawn 使用指定的命令行参数创建新进程语法格式如下child_process.spawn(command[, args][, options])参数参数说明如下command将要运行的命令argsArray 字符串参数数组options Objectcwd String 子进程的当前工作目录env Object 环境变量键值对stdio Array|String 子进程的 stdio 配置detached Boolean 这个子进程将会变成进程组的领导uid Number 设置用户进程的 IDgid Number 设置进程组的 IDspawn() 方法返回流 (stdout stderr)在进程返回大量数据时使用。进程一旦开始执行时 spawn() 就开始接收响应。实例让我们创建两个 js 文件 support.js 和 master.js。support.js 文件代码console.log(进程 process.argv[2] 执行。 );master.js 文件代码const fs require(fs); const child_process require(child_process); for(var i0; i3; i) { var workerProcess child_process.spawn(node, [support.js, i]); workerProcess.stdout.on(data, function (data) { console.log(stdout: data); }); workerProcess.stderr.on(data, function (data) { console.log(stderr: data); }); workerProcess.on(close, function (code) { console.log(子进程已退出退出码 code); }); }执行以上代码输出结果为$ node master.js stdout: 进程 0 执行。 子进程已退出退出码 0 stdout: 进程 1 执行。 子进程已退出退出码 0 stdout: 进程 2 执行。 子进程已退出退出码 0fork 方法child_process.fork 是 spawn() 方法的特殊形式用于创建进程语法格式如下child_process.fork(modulePath[, args][, options])参数参数说明如下modulePath String将要在子进程中运行的模块args Array 字符串参数数组optionsObjectcwd String 子进程的当前工作目录env Object 环境变量键值对execPath String 创建子进程的可执行文件execArgv Array 子进程的可执行文件的字符串参数数组默认 process.execArgvsilent Boolean 如果为true子进程的stdinstdout和stderr将会被关联至父进程否则它们将会从父进程中继承。默认为falseuid Number 设置用户进程的 IDgid Number 设置进程组的 ID返回的对象除了拥有ChildProcess实例的所有方法还有一个内建的通信信道。实例让我们创建两个 js 文件 support.js 和 master.js。support.js 文件代码console.log(进程 process.argv[2] 执行。 );master.js 文件代码const fs require(fs); const child_process require(child_process); for(var i0; i3; i) { var worker_process child_process.fork(support.js, [i]); worker_process.on(close, function (code) { console.log(子进程已退出退出码 code); }); }执行以上代码输出结果为$ node master.js 进程 0 执行。 子进程已退出退出码 0 进程 1 执行。 子进程已退出退出码 0 进程 2 执行。 子进程已退出退出码 0cluster一端口多进程的 HTTP 服务最小可用的多核 HTTP 服务实例// server-cluster.jsimport cluster from node:cluster;import os from node:os;import http from node:http;import { pbkdf2Sync } from node:crypto;if (cluster.isPrimary) {const cpuCount Math.max(1, os.cpus().length);console.log(主进程 ${process.pid}启动 ${cpuCount} 个工作进程);for (let i 0; i cpuCount; i) cluster.fork();cluster.on(exit, (worker, code) {console.warn(工作进程 ${worker.process.pid} 退出code${code}重启中…);cluster.fork();});} else {const server http.createServer((req, res) {// 模拟 CPU 密集计算pbkdf2Sync(password, salt, 100_000, 64, sha512);res.writeHead(200, {content-type:text/plain; charsetutf-8});res.end(Handled by worker ${process.pid}\n);});server.listen(3000, () {console.log(工作进程 ${process.pid} 监听 3000);});}运行node server-cluster.js多次请求 http://localhost:3000 会看到不同 PID 响应。注意线上多实例一般前置反向代理/负载均衡Nginx、Envoy、K8s Service或由 cluster 在一个端口聚合。WebSocket/粘性会话需要sticky session同一客户端落到同一 worker。可以用四层负载均衡的源地址哈希或在应用层自己做粘滞如按 req.socket.remoteAddress 做分发。优雅重启零停机思路思路主进程收到重载信号 → 先 fork 出新 worker 并等待其 listening → 再 disconnect 旧 worker等请求跑完退出。实例// 摘要示例在 cluster.isPrimary 分支里process.on(SIGUSR2, async () {console.log(收到 SIGUSR2开始优雅重启);const workers Object.values(cluster.workers ?? {});// 1) 启新const fresh cluster.fork();await new Promise(r fresh.once(listening, r));// 2) 逐个下线旧的for (const w of workers) {w?.disconnect();// 超时还没退就强杀setTimeout(() w?.process.kill(SIGKILL), 5000);}});Windows 没有 SIGUSR2可用 HTTP / RPC 管理接口触发重载。自建「进程池」控制并发、复用子进程当你有大量独立 CPU 任务如批量压缩、加密、爬虫解析频繁 fork/spawn 成本高这时需要 Process Pool 复用若干子进程像线程池一样限流。文件pool/worker.js实例// pool/worker.jsprocess.on(message, async (msg) {if (msg.type task) {const { id, payload } msg;// 模拟重任务斐波那契const fib (n) (n 1 ? n : fib(n-1) fib(n-2));const result fib(payload.n);process.send({ type: done, id, result });}});文件pool/index.js实例// pool/index.jsimport { fork } from node:child_process;import os from node:os;export class ProcessPool {constructor({ file, size Math.max(1, os.cpus().length - 1) } {}) {this.file file;this.size size;this.idle [];this.busy new Map(); // worker - taskIdthis.queue [];for (let i 0; i size; i) this._spawn();}_spawn() {const w fork(this.file);w.on(message, (m) {if (m?.type done) {const cb this.callbacks.get(m.id);if (cb) cb.resolve(m.result);this.callbacks.delete(m.id);this._markIdle(w);this._drain();}});w.on(exit, () {// 自动补齐池子this.busy.delete(w);const idx this.idle.indexOf(w);if (idx 0) this.idle.splice(idx, 1);this._spawn();});if (!this.callbacks) this.callbacks new Map();this.idle.push(w);}_markIdle(w) {this.busy.delete(w);if (!this.idle.includes(w)) this.idle.push(w);}_acquire() {return this.idle.length ? this.idle.shift() : null;}_drain() {while (this.queue.length this.idle.length) {const { id, payload, resolve, reject } this.queue.shift();const w this._acquire();this.busy.set(w, id);this.callbacks.set(id, { resolve, reject });w.send({ type: task, id, payload });}}runTask(payload) {const id Math.random().toString(36).slice(2);return new Promise((resolve, reject) {this.queue.push({ id, payload, resolve, reject });this._drain();});}close() {for (const w of this.idle) w.kill();for (const w of this.busy.keys()) w.kill();}}文件pool/demo.js实例// pool/demo.jsimport { ProcessPool } from ./index.js;const pool new ProcessPool({ file: ./pool/worker.js, size: 4 });const tasks Array.from({ length: 10 }, (_, i) pool.runTask({ n: 35 (i % 3) }));const t0 Date.now();const results await Promise.all(tasks);console.log(结果:, results);console.log(耗时(ms):, Date.now() - t0);await new Promise(r setTimeout(r, 100)); // 等待消息刷完pool.close();说明把任务封装成消息进程池负责分配与复用 worker。真实业务可把解析/转码/压缩封装到worker.js里。注意任务超时、重试、幂等与背压队列长度上限。实战小例HTTP 接口把重任务扔进进程池实例// app.jsimport http from node:http;import { ProcessPool } from ./pool/index.js;const pool new ProcessPool({ file: ./pool/worker.js, size: 4 });const server http.createServer(async (req, res) {if (req.url?.startsWith(/fib?)) {const url new URL(req.url, http://localhost);const n Number(url.searchParams.get(n) || 35);try {const result await pool.runTask({ n });res.writeHead(200, {content-type: application/json});res.end(JSON.stringify({ pid: process.pid, n, result }));} catch (e) {res.writeHead(500); res.end(error);}} else {res.writeHead(404).end(Not Found);}});server.listen(3000, () console.log(http://localhost:3000));访问/fib?n38等主进程不被阻塞任务在子进程里算。