← 返回题目列表

Node.js 中 worker_threads、child_process 和 cluster 应该如何选择?

高频 困难 第 16 / 27 题 更新于 2026/07/29
Node.jsworker_threadschild_processcluster

简化版

worker_threads 适合在同一进程内开线程处理 CPU 密集 JS 计算;child_process 适合启动独立进程、隔离崩溃或调用外部命令;cluster 适合多进程共享端口提升 Node 服务多核利用率。选择核心看:是否需要共享内存、是否要强隔离、是否是 Web 服务扩容。

详细版

三者都能“让任务离开主事件循环”,但边界不同:

  • worker_threads:线程级并行,适合 CPU 密集任务,可用 ArrayBufferSharedArrayBuffer 传递或共享数据。
  • child_process:进程级隔离,适合运行脚本、调用系统命令、隔离不可信或容易崩溃的任务。
  • cluster:基于多进程的服务扩容,让多个 worker 进程监听同一端口。

不要用 cluster 解决单个请求里的大计算,也不要用 worker_threads 承担不可信命令执行。前端 BFF 中如果有图片处理、Excel 解析、PDF 生成、大 JSON 计算,应考虑 worker 或独立任务服务。

完整版教学

一、为什么需要把任务移出主线程

Node.js 主线程负责执行 JavaScript 回调和推动事件循环。如果某个请求里做大计算,整个服务都会变慢。多核机器上,只跑一个 Node 主线程也无法充分利用 CPU。

app.get('/report', async (req, res) => {
  const data = buildHugeReport(req.query); // CPU 计算 1200ms
  res.json(data);
});

如果这段计算耗时 1200ms,期间其他请求即使只是读缓存,也要等主线程让出执行机会。worker、子进程和 cluster 都是在解决“主线程不能被长任务占住”这个问题,但解决角度不同。

CPU 任务 -> worker_threads
外部命令 / 强隔离 -> child_process
Web 服务多核扩容 -> cluster / 多进程管理器

二、worker_threads 适合 CPU 密集 JS

worker_threads 在同一进程内创建工作线程,每个 worker 有自己的 JS 执行环境,可以和主线程通过消息通信,也可以传递 ArrayBuffer 或使用 SharedArrayBuffer

// main.js
import { Worker } from 'node:worker_threads';

const worker = new Worker(new URL('./worker.js', import.meta.url), {
  workerData: { count: 50_000_000 }
});

worker.on('message', (result) => console.log(result));
worker.on('error', (err) => console.error(err));
// worker.js
import { parentPort, workerData } from 'node:worker_threads';

let sum = 0;
for (let i = 0; i < workerData.count; i++) sum += i;
parentPort.postMessage(sum);

worker 的优势是比进程更轻,适合频繁执行计算任务。代价是通信、序列化和线程管理也有成本。一个 5ms 的小任务丢给 worker,可能通信成本就抵消收益;一个 800ms 的计算任务才更值得隔离。

三、child_process 适合进程隔离和外部命令

child_process 会启动独立进程,常用 API 有 spawnexecexecFilefork。它适合调用系统命令、运行独立脚本、隔离容易崩溃的任务。

import { spawn } from 'node:child_process';

const ps = spawn('node', ['scripts/build-report.js'], {
  stdio: ['ignore', 'pipe', 'pipe']
});

ps.stdout.on('data', (chunk) => console.log(chunk.toString()));
ps.on('exit', (code) => console.log('exit', code));

spawn 更适合大输出流式处理,exec 会把输出缓存到内存,输出很大时容易出问题。假设命令输出 200 MB,exec 可能占用大量内存;spawn 可以边读边处理。

进程隔离的好处是子进程崩溃不一定拖垮主进程,权限、环境变量、资源限制也更容易隔离。代价是启动更重、通信更慢、内存占用更高。

四、cluster 适合 Web 服务多进程扩容

cluster 的目标是让多个 Node 进程共享服务端口,提高多核利用率。一个 master 或 primary 进程管理多个 worker 进程,每个 worker 都有自己的事件循环。

primary
  -> worker 1: event loop
  -> worker 2: event loop
  -> worker 3: event loop
  -> worker 4: event loop

假设 4 核机器上单进程 Node 服务 QPS 上限约 1000,合理多进程后可能接近 3000 到 4000,具体取决于数据库、网络、业务计算和负载均衡开销。cluster 解决的是服务实例并行,不是单个 JS 对象在多个进程中共享。

现代生产也常用 PM2、容器副本、Kubernetes Deployment 或进程管理器实现类似多进程扩容。面试时可以说 cluster 是 Node 内建方案,但工程上也会结合容器和网关负载均衡。

五、通信和数据传递成本决定了边界

把任务移出去不等于免费。主线程和 worker、子进程之间要通信,数据要复制、转移或共享。大对象传来传去会产生明显成本。

方案隔离级别通信成本适合任务
worker_threads线程隔离中等,可转移内存CPU 密集 JS
child_process进程隔离较高,IPC/stdio外部命令、强隔离
cluster多进程服务请求级分发Web 服务多核扩容
外部任务服务服务隔离网络成本重任务、可异步化业务

例如生成一个 20 MB 的报表,如果主线程把完整大对象传给 worker,再把完整结果传回来,光序列化和内存复制就可能很重。更好的设计可能是把输入压缩成任务参数,让 worker 自己读取数据源并输出文件路径。

记忆钩子:并行不是免费午餐。计算省下来的时间,要和启动、通信、复制、监控、失败处理的成本一起算。

六、工程选择要看失败和治理

worker 线程抛错,需要主线程监听 error 并决定是否重建 worker。子进程退出,需要处理 exit code、stderr、超时和资源清理。cluster worker 崩溃,需要拉起新进程并做好连接排空。

提交任务 -> 设置超时 -> 捕获错误 -> 记录上下文
       -> 释放资源 -> 重试或降级 -> 上报指标

不要只写“开 worker 就行”。真实服务还要限制并发任务数,否则 8 核机器上一次创建 1000 个 worker,会把 CPU 和内存打爆。通常会做 worker pool、任务队列和背压控制。

前端 Node BFF 中,如果只是请求聚合和轻量字段处理,不需要 worker;如果要做图片压缩、PDF 渲染、大文件解析、复杂规则计算,就要考虑隔离方案。

七、常见误区与追问

  • 误区:cluster 可以让一个请求里的大计算自动变快。 cluster 增加的是进程数量,单个请求分到某个 worker 后仍可能阻塞该进程。
  • 误区:worker_threads 比 child_process 永远更好。 worker 更轻但隔离弱,执行不可信命令或需要强隔离时子进程更合适。
  • 误区:任务丢出去就没有性能成本。 通信、序列化、内存复制、启动和调度都要成本,小任务可能不划算。
  • 追问:CPU 密集任务为什么不用 libuv 线程池自动处理? 普通 JS 大循环不会自动进入 libuv 线程池,需要 worker 或进程隔离。
  • 追问:spawn 和 exec 怎么选? 大输出、长运行任务用 spawn 流式处理;小命令且需要完整输出时可用 exec,但要注意缓冲区。
  • 追问:生产中 cluster 之外还有什么方案? PM2、容器多副本、Kubernetes、进程管理器和网关负载均衡都可实现多实例扩容。

八、加强记忆

选择题可以记成“三把刀”:worker_threads 切 CPU 计算,child_process 切进程隔离和外部命令,cluster 切 Web 服务多核扩容。再补一句成本意识:任务越重越值得隔离,数据越大越要减少传输,崩溃越危险越要强隔离。