进程与集群
Node 默认是单进程、单线程的。单进程意味着一次只能跑一份 JS 代码,也意味着如果进程崩溃服务就停了。通过 child_process 开启子进程、用 cluster 集群化部署、用 worker_threads 处理 CPU 密集任务,才能把多核机器的算力真正用起来。
一、process 对象
process 是全局对象,代表当前进程:
console.log(process.pid); // 进程 ID
console.log(process.ppid); // 父进程 ID
console.log(process.cwd()); // 当前工作目录
console.log(process.argv); // 命令行参数
console.log(process.env); // 环境变量
console.log(process.uptime()); // 进程已运行秒数
console.log(process.memoryUsage()); // 内存使用(rss、heapUsed 等)
console.log(process.platform); // 平台:win32 / linux / darwin
console.log(process.arch); // CPU 架构:x64 / arm64| 属性/方法 | 作用 |
|---|---|
process.pid / ppid | 当前进程 / 父进程 ID |
process.argv | 命令行参数数组(前两项是 node 与脚本路径) |
process.env | 环境变量对象 |
process.cwd() | 当前工作目录 |
process.uptime() | 进程运行时长(秒) |
process.memoryUsage() | 内存统计(heapUsed 是 JS 堆占用) |
process.exitCode | 退出码(让进程自然结束) |
// 解析命令行参数
const args = process.argv.slice(2);
console.log(args); // ['--port', '8080']
const portIndex = args.indexOf("--port");
if (portIndex !== -1) console.log("端口:", args[portIndex + 1]);二、退出码与优雅退出
2.1 退出码约定
| 退出码 | 含义 |
|---|---|
| 0 | 成功退出 |
| 1 | 未捕获异常、一般错误 |
| 2 | 命令行参数错误 |
| 128 + 信号编号 | 被信号杀死(如 SIGINT 是 130) |
// 两种退出方式
process.exit(1); // 立即退出,未完成的异步操作会被直接丢弃
process.exitCode = 1; // 设置退出码,进程正常走完事件循环后退出2.2 exit 与 beforeExit
process.on("beforeExit", (code) => {
// 事件循环即将清空时触发;在这里注册异步任务会让进程继续
console.log("事件循环空了,即将退出");
});
process.on("exit", (code) => {
// 只能执行同步代码!异步回调在这里不会被执行
console.log("进程退出,码:", code);
});| 事件 | 触发时机 | 能否做异步操作 |
|---|---|---|
beforeExit | 事件循环清空、尚未退出 | 可以(会推迟退出) |
exit | 进程真正退出前 | 只能同步 |
uncaughtException | 有未捕获异常 | 需自行兜底 |
2.3 未捕获异常兜底
process.on("uncaughtException", (err) => {
console.error("捕获到未处理异常:", err.message);
process.exitCode = 1;
});
process.on("unhandledRejection", (reason) => {
console.error("未处理的 Promise 拒绝:", reason);
});三、child_process:三种子进程方式
child_process 模块用于创建子进程,三个主力 API:
| 方法 | 通信方式 | 适合场景 | 获取输出 |
|---|---|---|---|
exec | 通过回调 | 执行简单命令、拿完整输出 | 一次性回调(字符串) |
execFile | 通过回调 | 执行可执行文件(不走 shell) | 一次性回调 |
spawn | 流式管道 | 长时运行、大数据量输出 | stdout/stderr 流 |
fork | IPC 消息 | 运行另一个 Node 脚本并通信 | 基于 spawn + IPC |
const { exec, spawn, fork } = require("child_process");
// exec:走 shell,输出作为整体回调
exec("dir", (err, stdout, stderr) => {
if (err) return console.error("失败:", err.message);
console.log("目录内容:", stdout);
});
// spawn:不经过 shell,参数以数组传递,输出是流
const ls = spawn("ls", ["-la"]);
ls.stdout.on("data", (chunk) => process.stdout.write(chunk));安全提醒:
exec会拼进 shell 执行,命令中若包含用户输入(如exec("rm " + userInput))极易造成命令注入。拼接动态内容请用spawn+ 参数数组,或execFile。
四、spawn 详解
spawn 是底层能力最完整的子进程 API:
const { spawn } = require("child_process");
// 启动一个长时命令
const child = spawn("node", ["worker.js"], { cwd: __dirname });
// 子进程的三种标准流
child.stdout.on("data", (data) => console.log("stdout:", data.toString()));
child.stderr.on("data", (data) => console.error("stderr:", data.toString()));
// 向子进程的 stdin 写入
child.stdin.write("hello\n");
child.stdin.end();
// 退出事件
child.on("exit", (code, signal) => {
console.log(`子进程退出,code=${code},signal=${signal}`);
});
child.on("error", (err) => console.error("启动失败:", err.message));| 事件 | 触发时机 |
|---|---|
data(stdout/stderr) | 子进程输出数据 |
exit | 子进程退出(流可能仍有余量) |
close | 子进程退出且所有流关闭 |
error | 无法启动、被杀等错误 |
4.1 exec 与 execFile 区别
// exec:使用 shell,支持管道与通配符
exec("echo hello | wc -l", (err, stdout) => console.log(stdout.trim())); // 1
// execFile:直接执行文件,不经 shell,更安全、更省资源
execFile("node", ["-v"], (err, stdout) => console.log(stdout.trim())); // v20.x.x| 对比项 | exec | execFile |
|---|---|---|
| 是否经过 shell | 是 | 否 |
| 参数注入风险 | 高 | 低 |
| 输出方式 | 缓冲后一次性回调 | 缓冲后一次性回调 |
| 适用 | 简单命令 | 明确的可执行文件 |
五、父子进程通信(IPC)
fork 专为运行 Node 子进程并通信设计,通过 send 发送消息、message 事件接收:
// parent.js
const { fork } = require("child_process");
const child = fork("./child.js");
child.on("message", (msg) => {
console.log("收到子进程消息:", msg);
child.send({ from: "parent", ack: true });
});
child.send({ from: "parent", task: "计算 1+1" });// child.js
process.on("message", (msg) => {
console.log("子进程收到:", msg);
const result = msg.task === "计算 1+1" ? 2 : 0;
process.send({ from: "child", result }); // 发回父进程
});node parent.js
# 输出:收到子进程消息:{ from: 'child', result: 2 }| 术语 | 说明 |
|---|---|
child.send(msg) | 父进程向子进程发消息 |
process.send(msg) | 子进程向父进程发消息 |
message 事件 | 任意一侧收到消息时触发 |
| 消息内容 | 结构化的 JS 对象(自动序列化) |
IPC 消息不是简单 JSON:复杂对象(如 Buffer、Date)会被特殊处理,大对象传输有开销,适合传小数据或命令。
六、cluster 集群
Node 单进程只能用一个 CPU 核心。cluster 模块让多个 worker 进程共享同一个端口,把请求分散到各核心:
const cluster = require("cluster");
const http = require("http");
const os = require("os");
if (cluster.isPrimary) {
// 主进程:fork 出与 CPU 核数相等的 worker
const cpus = os.cpus().length;
for (let i = 0; i < cpus; i++) {
cluster.fork();
}
// worker 退出后自动重启
cluster.on("exit", (worker, code) => {
console.log(`worker ${worker.process.pid} 退出,正在重启...`);
cluster.fork();
});
} else {
// worker 进程:每个都跑同一份 HTTP 服务
http.createServer((req, res) => {
res.end(`由进程 ${process.pid} 处理`);
}).listen(3000);
}| 概念 | 说明 |
|---|---|
cluster.isPrimary | 当前是否主进程(Node 16+,旧版叫 isMaster) |
cluster.fork() | 创建 worker 进程 |
| worker 共享端口 | 底层由主进程接收连接再分发 |
| 自动重启 | 监听 exit 事件并 fork() 新 worker |
6.1 worker 管理
// 遍历 worker
for (const id in cluster.workers) {
const worker = cluster.workers[id];
console.log(worker.process.pid);
worker.send({ type: "shutdown" }); // 向指定 worker 发消息
worker.kill(); // 结束该 worker
}
cluster.on("message", (worker, msg) => {
console.log(`来自 worker ${worker.process.pid}:`, msg);
});6.2 重启策略要点
| 策略 | 做法 |
|---|---|
| 限制重启频率 | 短时间内频繁崩溃时暂停重启,避免重启风暴 |
| 优雅退出 | 收到关闭信号先停止接新请求,处理完存量再退出 |
| 健康检查 | 定时探测 worker 是否僵死 |
七、worker_threads 线程
worker_threads 提供真正的线程(同一进程内),适合 CPU 密集任务,如压缩、加解密、图像处理:
// main.js
const { Worker } = require("worker_threads");
const worker = new Worker("./compute.js", { workerData: 40 });
worker.on("message", (result) => {
console.log("计算结果:", result);
});
worker.on("error", (err) => console.error(err));// compute.js
const { parentPort, workerData } = require("worker_threads");
// 计算斐波那契数列,占用主线程会卡住服务,放线程里做
function fib(n) {
return n < 2 ? n : fib(n - 1) + fib(n - 2);
}
parentPort.postMessage(fib(workerData));7.1 进程与线程对比
| 对比项 | child_process | worker_threads |
|---|---|---|
| 隔离级别 | 独立进程,内存完全隔离 | 同进程,共享内存区(可传 SharedArrayBuffer) |
| 通信方式 | IPC 消息序列化 | postMessage + 结构化克隆(更快) |
| 适合场景 | 跑外部命令、隔离崩溃、多实例部署 | CPU 密集计算 |
| 崩溃影响 | 只影响自己 | 会拖累整个进程 |
| 启动开销 | 较大 | 较小 |
| 场景 | 选型 |
|---|---|
| 执行 shell 命令、系统工具 | child_process.exec / spawn |
| 运行另一个 Node 脚本并高频通信 | fork |
| Web 服务多核部署 | cluster |
| 纯计算(加密、压缩、图像处理) | worker_threads |
八、进程信号处理
信号是操作系统通知进程的方式。服务端程序最常见的两个信号:SIGINT(Ctrl+C)与 SIGTERM(kill 命令默认发送,Docker 停止容器也发它)。
const http = require("http");
const server = http.createServer((req, res) => res.end("ok"));
server.listen(3000);
// 优雅退出:先停止接收新连接,再释放资源,最后退出
function shutdown(signal) {
console.log(`收到 ${signal},开始优雅退出...`);
server.close(() => {
console.log("所有连接已关闭");
process.exit(0);
});
// 兜底:10 秒内没关完就强制退出
setTimeout(() => process.exit(1), 10_000).unref();
}
process.on("SIGINT", () => shutdown("SIGINT")); // Ctrl+C
process.on("SIGTERM", () => shutdown("SIGTERM")); // kill 默认信号| 信号 | 来源 | 默认行为 |
|---|---|---|
SIGINT | Ctrl+C、kill -2 | 终止进程 |
SIGTERM | kill(默认)、Docker stop | 终止进程 |
SIGHUP | 终端挂断 | 终止进程 |
SIGKILL | kill -9 | 直接杀死,无法监听 |
SIGUSR1 / SIGUSR2 | 用户自定义 | 可用于触发重载配置 |
SIGKILL无法被捕获,process.on("SIGKILL")无效。优雅退出依赖应用主动监听可捕获的信号。
进程是 Node 服务化的骨架:单进程负责逻辑,子进程扩展能力,cluster 提升吞吐,worker_threads 扛住计算。配合信号处理与重启策略,才能构建出健壮的服务。