Node.js多进程和 Cluster 集群部署,别让一个进程扛完整台机器

简介: Node.js单进程无法利用多核,易成性能瓶颈。Cluster模块通过主从进程模型,让多个Worker共享端口、分担请求,提升并发与容错能力;但进程内存不共享,需外置状态、带pid日志、优雅退出——它不是银弹,而是多核落地的起点。(239字)

Node.js多进程和 Cluster 集群部署,别让一个进程扛完整台机器

Node 的单进程模型很好理解,也很好踩坑

你在一台 8 核机器上启动一个 Express 服务,如果没有额外处理,真正跑业务 JS 的就是一个进程。CPU 一个核心打满了,旁边几个核心还在看戏。线上流量一上来,表现就很怪:机器总体 CPU 看着不高,接口 p99 却开始抖

这篇讲 Node.js 多进程和 Cluster。环境口径:Node.js 24、Express 5.2.1。先用 Node 官方 cluster 模块写一个能跑的版本,再说它的边界,以及为什么生产里很多团队最后会交给 PM2、systemd、Docker 或 Kubernetes 管进程

Cluster 解决的是什么问题

Node 官方文档对 cluster 的描述很直白:它可以创建一组子进程,这些子进程共享服务器端口

这里的关键词是进程

Cluster 不是线程池,也不是把一个请求拆给多个核心一起算。它的做法更像是:主进程负责拉起 worker,多个 worker 都监听同一个端口,请求来了之后分发给其中一个 worker 处理

这带来两个直接好处

多核 CPU 能用起来

某个 worker 崩了,主进程可以拉一个新的

代价也很明确:进程之间内存不共享。你放在内存里的登录态、计数器、缓存,每个 worker 都有自己的一份。只要上了多进程,就不要再把进程内存当成全局真相

一个最小可跑的 Cluster 服务

先装 Express

mkdir node-cluster-demo
cd node-cluster-demo
npm init -y
npm install express

server.js

const cluster = require('node:cluster')
const os = require('node:os')
const process = require('node:process')
const express = require('express')

const port = Number(process.env.PORT || 3000)
const workerCount = Number(process.env.WORKERS || os.availableParallelism())

if (cluster.isPrimary) {
   
  console.log(`primary ${
     process.pid} is running`)
  console.log(`starting ${
     workerCount} workers`)

  for (let i = 0; i < workerCount; i++) {
   
    cluster.fork()
  }

  cluster.on('exit', (worker, code, signal) => {
   
    console.error(`worker ${
     worker.process.pid} died`, {
    code, signal })
    cluster.fork()
  })
} else {
   
  const app = express()

  app.get('/health', (req, res) => {
   
    res.json({
   
      ok: true,
      pid: process.pid,
    })
  })

  app.get('/cpu', (req, res) => {
   
    const startedAt = Date.now()
    while (Date.now() - startedAt < 80) {
   
      Math.sqrt(Math.random())
    }

    res.json({
   
      ok: true,
      pid: process.pid,
    })
  })

  app.listen(port, () => {
   
    console.log(`worker ${
     process.pid} listening on http://localhost:${
     port}`)
  })
}

启动:

node server.js

多请求几次:

curl http://localhost:3000/health
curl http://localhost:3000/health
curl http://localhost:3000/health

你会看到返回的 pid 可能不同,说明请求落到了不同 worker 上

这里用 os.availableParallelism() 而不是老习惯里的 os.cpus().length。Node 官方在 os.cpus() 文档里也提醒过,不应该用它来计算应用可用并行度,推荐用 os.availableParallelism()

生产代码不能只会 fork

上面的代码能跑,但离生产还差几步。至少要处理优雅退出

function createApp() {
   
  const app = express()

  app.get('/health', (req, res) => {
   
    res.json({
    ok: true, pid: process.pid })
  })

  return app
}

if (cluster.isPrimary) {
   
  for (let i = 0; i < workerCount; i++) {
   
    cluster.fork()
  }

  cluster.on('exit', (worker, code, signal) => {
   
    if (worker.exitedAfterDisconnect) {
   
      return
    }

    console.error(`worker ${
     worker.process.pid} crashed`, {
    code, signal })
    cluster.fork()
  })
} else {
   
  const app = createApp()
  const server = app.listen(port)

  process.on('SIGTERM', () => {
   
    server.close(() => {
   
      process.exit(0)
    })

    setTimeout(() => {
   
      process.exit(1)
    }, 10_000).unref()
  })
}

server.close() 会停止接收新连接,已有连接处理完再退出。外面再加一个 10 秒兜底,是为了防止某些长连接或异常请求让进程永远退不掉

主进程里也别无脑重启所有退出的 worker。worker.exitedAfterDisconnect 可以帮你区分“自己要求它退出”和“它崩了”。滚动重启时尤其有用

状态别放在进程内存里

这是 Cluster 最容易坑人的地方

假设你写了一个简单计数器:

let counter = 0

app.post('/count', (req, res) => {
   
  counter += 1
  res.json({
    counter, pid: process.pid })
})

单进程时它看起来没问题。多进程后,每个 worker 都有自己的 counter。请求打到 worker A,counter 是 10。下一个请求打到 worker B,counter 可能是 3。你以为自己写了全局计数器,其实写了 N 个局部计数器

登录态也一样。不要把 session 存在进程内存里,然后指望 Cluster 替你同步。放 Redis、数据库,或者用无状态 token。进程内缓存也要接受一个现实:每个 worker 都会各自缓存一份,命中率和内存占用都会受影响

我见过一个后台系统,上 Cluster 之后验证码偶发校验失败。原因很土:验证码存在内存 Map 里,生成请求落到 worker 1,校验请求落到 worker 3,当然找不到

负载均衡不是魔法

Node cluster 支持两类分发方式。官方文档里提到,除 Windows 外,默认是 round-robin,由主进程接受连接再分发给 worker。另一种方式是主进程创建监听 socket 后交给 worker,worker 自己 accept

日常业务一般不用手动改 cluster.schedulingPolicy。你真正要关心的是:worker 之间的负载可能仍然不均匀

原因有很多

请求耗时不同,慢请求会把某个 worker 占住

长连接,比如 WebSocket,会让连接长期停在某个 worker 上

CPU 密集逻辑会堵住单个 worker 的 event loop

所以 Cluster 不是性能银弹。它只是让你把请求分散到多个进程。某个接口本身写得很重,多进程能缓解,但不能治本

多进程下日志和监控要带 pid

上了 Cluster 之后,日志里必须带 pid 或 worker id

logger.info('request finished', {
   
  pid: process.pid,
  workerId: cluster.worker?.id,
  method: req.method,
  path: req.originalUrl,
  status: res.statusCode,
})

否则你看到一段错误日志,很难判断是不是某一个 worker 持续出问题。比如 worker 4 内存一直涨,最后反复重启。总览日志里只看得到“进程重启了”,看不到是哪一个,这就很难定位

监控也一样。除了进程整体指标,我会给每个 worker 都打:

  • RSS、heapUsed
  • event loop delay
  • 请求数和错误数
  • 重启次数

多进程系统最怕平均值。平均 CPU 不高,不代表每个 worker 都健康。平均内存正常,也可能其中一个 worker 正在泄漏

Cluster 和 PM2、容器怎么取舍

如果你只是想理解原理,Node 内置 cluster 很适合

如果你要生产部署,我会按环境选

传统服务器,可以用 PM2 的 cluster mode 或 systemd 管理多个进程。PM2 省心,日志、重启、进程列表都有现成命令。systemd 更贴近系统层,适合不想引入额外 Node 进程管理工具的团队

Docker / Kubernetes 环境,我更倾向于一个容器一个 Node 进程,然后用多个副本扩容。进程重启、健康检查、滚动发布、日志采集都交给平台。你也可以在容器内再开 Cluster,但这样会让资源限制、优雅退出、监控粒度变复杂。不是不能做,只是要有明确理由

本地开发和小型内网服务,直接 cluster 也够用。但别把它当完整的进程平台。它不负责日志采集、发布编排、健康检查、限流熔断,也不会替你处理共享状态

什么场景不适合只靠 Cluster

CPU 重任务。Cluster 能把请求分到多个进程,但单个请求里的 CPU 计算还是会堵住对应 worker。重计算更适合 Worker Threads、任务队列,或者拆到独立服务

大量 WebSocket 长连接。连接会固定在某个 worker 上,负载均衡和会话管理要额外设计。多实例部署时还要考虑消息广播和房间状态

强依赖本地内存状态的应用。比如本地 Map 存 session、验证码、临时任务状态。先把状态外置,再谈多进程

超短任务但日志极重的服务。进程多了,日志竞争和 IO 压力也会上来。这个时候要先控制日志量

收尾

Cluster 的价值很朴素:让 Node 服务用上多核,并且给 worker 崩溃后的恢复留一个入口

但它也会逼你面对几个工程事实

进程内存不是共享状态

日志和监控必须能区分 worker

优雅退出要自己处理

多进程只能缓解单进程瓶颈,不能修好糟糕的同步代码

我的建议是:先用 cluster 把多进程模型跑明白,再根据部署环境决定交给谁管。传统机器上用 PM2 或 systemd,容器环境交给 Kubernetes 或编排平台。无论选哪条路,别让一个 Node 进程孤零零扛完整台机器


参考来源

目录
相关文章
|
网络协议 关系型数据库 Shell
gitlab-设置邮件SMTP以及GitLab收不到邮件的问题
gitlab-设置邮件SMTP以及GitLab收不到邮件的问题
1664 1
|
存储 缓存 Java
JSP页面生命周期详解及优化建议
JSP页面生命周期详解及优化建议
436 0
|
27天前
|
Web App开发 人工智能 Cloud Native
一人买多用不完,多人分享被封号——"Key池化"破解 AI 订阅共享困局
Claude用户面临“独用浪费、共享封号”困局:Max 20x额度闲置,多人拼车却因IP跳变、指纹泄露等触发风控。Key池化方案通过本地代理+虚拟Key分发,实现额度共享而不共号,规避风控,降低成本(3人仅$200/月),提升安全与体验。
394 7
|
1月前
|
开发者
一个自动修复bug任务,运行了100分钟,直接把免费次数,付费次数全干没了
“不得不服”是一款自动修复Bug的工具,但某次任务异常耗时100分钟,行为不明,竟耗尽全部免费及付费次数,引发开发者惊叹与调侃。
283 2
|
Ubuntu 开发工具
Ubuntu更换阿里云软件源
Ubuntu更换阿里云软件源
147725 0
|
27天前
|
JavaScript Shell Linux
Node.js 环境搭建与版本管理,用 nvm 把多版本问题一次性解决(2026)
本文是2026年最新Node.js环境搭建指南,详解如何用nvm(macOS/Linux)或nvm-windows/fnm(Windows)一站式解决多版本冲突问题。涵盖彻底卸载旧版、可视化/命令行安装、LTS版本选择、`.nvmrc`自动切换、国内镜像加速及常见坑点排查,助开发者10分钟完成零配置到多项目自由切换。(239字)
602 0
|
存储 运维 开发工具
警惕日志采集失败的 6 大经典雷区:从本地管理反模式到 LoongCollector 标准实践
本文探讨了日志管理中的常见反模式及其潜在问题,强调科学的日志管理策略对系统可观测性的重要性。文中分析了6种反模式:copy truncate轮转导致的日志丢失或重复、NAS/OSS存储引发的采集不一致、多进程写入造成的日志混乱、创建文件空洞释放空间的风险、频繁覆盖写带来的数据完整性问题,以及使用vim编辑日志文件导致的重复采集。针对这些问题,文章提供了最佳实践建议,如使用create模式轮转日志、本地磁盘存储、单线程追加写入等方法,以降低日志采集风险,提升系统可靠性。最后总结指出,遵循这些实践可显著提高故障排查效率和系统性能。
2586 23
|
10月前
|
监控 安全 开发者
2025年高教社杯D题——矿井突水水流漫延模型与逃生方案全国大学生数学建模(思路、代码、论文)
2025年高教社杯D题——矿井突水水流漫延模型与逃生方案全国大学生数学建模(思路、代码、论文)
434 2
|
人工智能 运维 自然语言处理
通义灵码 AI实战《手把手教你用通义灵码写一个音乐电子小闹钟》
通义灵码DeepSeek版本相比qwen2.5,增强了深度思考和上下文理解能力,显著提升了开发效率,尤其适合代码能力较弱的运维人员,真正实现了“代码即服务”。
581 4
|
机器学习/深度学习 数据采集 算法
回归算法全解析!一文读懂机器学习中的回归模型
回归算法全解析!一文读懂机器学习中的回归模型
1924 0