Node.js 的单线程事件循环模型让它在 I/O 密集型场景下表现出色,但也带来了一个致命问题:它默认只能使用一个 CPU 核心。在一台 8 核服务器上,Node.js 默认只利用了 12.5% 的算力。根据 Node.js 官方性能基准测试,启用 Cluster 模块后,HTTP 请求吞吐量可以提升 3-7 倍(取决于 CPU 核心数和请求类型)。如果你的生产服务器还在跑单进程 Node.js,这篇文章就是为你写的。
🔧 一、Cluster 模块:Node.js 原生多进程方案
Node.js 内置的 cluster 模块是实现多进程的最基础方案。它通过 child_process.fork() 创建多个工作进程(Worker),共享同一个服务端口,由操作系统或 Node.js 内部进行负载均衡。
1.1 Cluster 的工作原理
Cluster 模块采用 主-从(Master-Worker)架构。主进程(Master)负责监听端口并接受连接,然后将连接分发给工作进程。分发策略有两种:
- 轮询(Round-Robin):默认策略(除 Windows 外),主进程按顺序将连接分配给每个 Worker
- 操作系统调度:由操作系统自行决定哪个进程处理连接,可能出现负载不均
⚠️ **警告:**在生产环境中,务必使用默认的轮询策略。操作系统调度在 Linux 上可能导致连接分配不均,某些 Worker 空闲而另一些过载。
1.2 基础实现代码
下面是一个完整的 Cluster 服务器实现,包含优雅退出和自动重启:
// cluster-basic.js - 基础集群服务器实现
const cluster = require('node:cluster');
const http = require('node:http');
const os = require('node:os');
const process = require('node:process');
const numCPUs = os.cpus().length;
const PORT = 3000;
if (cluster.isPrimary) {
console.log(`主进程 ${process.pid} 正在运行`);
console.log(`启动 ${numCPUs} 个工作进程...`);
// 记录 Worker 重启次数,防止无限重启
const restartCounts = new Map();
const MAX_RESTARTS = 5;
const RESTART_WINDOW = 60000; // 60 秒内
// Fork 工作进程
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// 监听 Worker 退出事件
cluster.on('exit', (worker, code, signal) => {
const pid = worker.process.pid;
const now = Date.now();
const record = restartCounts.get(pid) || { count: 0, firstAt: now };
// 重置计数器(超过时间窗口)
if (now - record.firstAt > RESTART_WINDOW) {
record.count = 0;
record.firstAt = now;
}
record.count++;
restartCounts.set(pid, record);
if (record.count > MAX_RESTARTS) {
console.error(`Worker ${pid} 在 ${RESTART_WINDOW / 1000}s 内重启超过 ${MAX_RESTARTS} 次,停止重启`);
return;
}
console.log(`Worker ${pid} 已退出 (code: ${code}, signal: ${signal}),正在重启...`);
cluster.fork();
});
// 监听 Worker 在线事件
cluster.on('online', (worker) => {
console.log(`Worker ${worker.process.pid} 已上线`);
});
} else {
// 工作进程:创建 HTTP 服务器
const server = http.createServer((req, res) => {
// 模拟 CPU 密集型任务
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += Math.sqrt(i);
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
pid: process.pid,
message: `Hello from Worker ${process.pid}`,
computation: sum.toFixed(2)
}));
});
server.listen(PORT, () => {
console.log(`Worker ${process.pid} 开始监听端口 ${PORT}`);
});
// 优雅退出:接收主进程信号
process.on('SIGTERM', () => {
console.log(`Worker ${process.pid} 收到 SIGTERM,正在优雅退出...`);
server.close(() => {
console.log(`Worker ${process.pid} 已关闭所有连接`);
process.exit(0);
});
// 超时强制退出
setTimeout(() => {
console.error(`Worker ${process.pid} 优雅退出超时,强制退出`);
process.exit(1);
}, 10000);
});
}
1.3 Cluster 模块的局限性
虽然 Cluster 模块是 Node.js 内置方案,但在生产环境中直接使用存在以下问题:
| 特性 | Cluster 模块 | PM2 | Worker Threads |
|---|---|---|---|
| 进程管理 | ❌ 手动实现 | ✅ 自动管理 | ❌ 手动实现 |
| 负载均衡 | ✅ 轮询 | ✅ 轮询 | ❌ 需手动实现 |
| 零停机重启 | ❌ 需手动实现 | ✅ 内置 | ❌ 不支持 |
| 监控面板 | ❌ 无 | ✅ 内置 | ❌ 无 |
| 日志管理 | ❌ 需自行实现 | ✅ 内置 | ❌ 需自行实现 |
| 内存限制 | ⚠️ 每进程独立 | ✅ 自动重启 | ⚠️ 共享内存 |
| 适用场景 | 简单多进程 | 生产环境 | CPU 密集计算 |
💡 **提示:**如果你只是想快速让 Node.js 用满多核 CPU,直接用 PM2 就够了。Cluster 模块更适合需要精细控制进程行为的场景。
🚀 二、PM2:生产级进程管理器
PM2 是 Node.js 生态中最流行的进程管理器,它封装了 Cluster 模块,并提供了进程守护、日志管理、监控、零停机部署等生产级功能。
2.1 PM2 集群模式配置
// ecosystem.config.js - PM2 生产环境配置
module.exports = {
apps: [
{
name: 'api-server',
script: './dist/server.js',
instances: 'max', // 使用所有 CPU 核心
exec_mode: 'cluster', // 集群模式
autorestart: true, // 自动重启
watch: false, // 生产环境关闭文件监听
max_memory_restart: '1G', // 内存超过 1G 自动重启
interpreter: 'node',
interpreter_args: '--max-old-space-size=1024',
// 环境变量
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 8080
},
env_staging: {
NODE_ENV: 'staging',
PORT: 8081
},
// 日志配置
log_file: './logs/combined.log',
error_file: './logs/error.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true, // 合并集群日志
// 优雅退出配置
kill_timeout: 5000, // 5 秒后强制退出
listen_timeout: 8000, // 8 秒内未就绪则重启
shutdown_with_message: true,
// 健康检查
health_check: {
path: '/health',
interval: 30000, // 每 30 秒检查一次
timeout: 5000,
max_consecutive_failures: 3
}
}
],
// 部署配置(可选)
deploy: {
production: {
user: 'deploy',
host: ['server1.example.com', 'server2.example.com'],
ref: 'origin/main',
repo: 'git@github.com:user/app.git',
path: '/var/www/production',
'post-deploy': 'npm install && npm run build && pm2 reload ecosystem.config.js --env production',
'pre-setup': 'apt install nodejs npm -y'
}
}
};
2.2 零停机部署(Graceful Reload)
零停机部署是 PM2 最实用的功能之一。它通过逐个重启 Worker 来实现,确保在更新过程中始终有 Worker 在处理请求:
# 零停机重载(推荐用于生产环境)
pm2 reload ecosystem.config.js --env production
# 查看进程状态
pm2 list
# 查看实时监控
pm2 monit
# 查看日志
pm2 logs api-server --lines 100
# 查看进程详细信息
pm2 show api-server
📌 记住:
pm2 reload和pm2 restart的区别在于:reload是逐个重启 Worker(零停机),而restart是同时重启所有进程(有停机时间)。生产环境务必使用reload。
2.3 自定义优雅退出信号处理
在集群模式下,PM2 会向 Worker 发送 SIGINT 信号来触发优雅退出。你需要在应用中正确处理这个信号:
// graceful-shutdown.js - 生产级优雅退出实现
const http = require('node:http');
const server = http.createServer(async (req, res) => {
if (req.url === '/health') {
// 健康检查端点
res.writeHead(200);
res.end('OK');
return;
}
// 模拟请求处理
try {
const result = await handleRequest(req);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
} catch (err) {
res.writeHead(500);
res.end('Internal Server Error');
}
});
// 追踪活跃连接,用于优雅退出
const activeConnections = new Set();
server.on('connection', (conn) => {
activeConnections.add(conn);
conn.on('close', () => activeConnections.delete(conn));
});
// 是否正在关闭
let isShuttingDown = false;
async function gracefulShutdown(signal) {
if (isShuttingDown) return;
isShuttingDown = true;
console.log(`[PID: ${process.pid}] 收到 ${signal},开始优雅退出...`);
// 1. 停止接受新连接
server.close(() => {
console.log(`[PID: ${process.pid}] HTTP 服务器已关闭`);
});
// 2. 等待活跃请求完成(最多 5 秒)
const deadline = setTimeout(() => {
console.warn(`[PID: ${process.pid}] 超时,强制关闭 ${activeConnections.size} 个连接`);
for (const conn of activeConnections) {
conn.destroy();
}
process.exit(1);
}, 5000);
// 3. 关闭数据库连接、Redis 连接等
await closeDatabaseConnections();
await closeRedisConnections();
// 4. 等待所有请求完成
while (activeConnections.size > 0) {
console.log(`[PID: ${process.pid}] 等待 ${activeConnections.size} 个连接关闭...`);
await new Promise((resolve) => setTimeout(resolve, 100));
}
clearTimeout(deadline);
console.log(`[PID: ${process.pid}] 优雅退出完成`);
process.exit(0);
}
// 监听退出信号
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
// PM2 集群模式下的消息处理
process.on('message', (msg) => {
if (msg === 'shutdown') {
gracefulShutdown('PM2 shutdown');
}
});
// 未捕获异常处理
process.on('uncaughtException', (err) => {
console.error('[FATAL] 未捕获异常:', err);
gracefulShutdown('uncaughtException');
});
process.on('unhandledRejection', (reason) => {
console.error('[FATAL] 未处理的 Promise 拒绝:', reason);
gracefulShutdown('unhandledRejection');
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`[PID: ${process.pid}] 服务器启动在端口 ${PORT}`);
// 通知 PM2 进程已就绪
if (process.send) {
process.send('ready');
}
});
// 模拟函数(实际项目中替换为真实逻辑)
async function handleRequest(req) {
return { status: 'ok', pid: process.pid };
}
async function closeDatabaseConnections() {
// 关闭数据库连接池
}
async function closeRedisConnections() {
// 关闭 Redis 连接
}
⚡ 三、Worker Threads:真正的多线程
Cluster 模块创建的是独立进程,每个进程有自己的 V8 实例和内存空间。而 Worker Threads 创建的是同一进程内的线程,可以共享内存(通过 SharedArrayBuffer)。两者适用场景不同:
- Cluster:适合 I/O 密集型的 Web 服务器,每个 Worker 独立处理请求
- Worker Threads:适合 CPU 密集型计算,需要共享数据或低开销通信
3.1 CPU 密集型任务的 Worker Thread 实现
// worker-main.js - 主线程:分配 CPU 密集型任务
const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');
const os = require('node:os');
const path = require('node:path');
if (isMainThread) {
const http = require('node:http');
const numCPUs = os.cpus().length;
// Worker 池
class WorkerPool {
constructor(workerPath, poolSize) {
this.workerPath = workerPath;
this.poolSize = poolSize;
this.workers = [];
this.taskQueue = [];
this.activeWorkers = 0;
// 预创建 Worker
for (let i = 0; i < poolSize; i++) {
this.workers.push(this.createWorker());
}
}
createWorker() {
const worker = new Worker(this.workerPath);
worker.busy = false;
worker.on('message', (result) => {
worker.busy = false;
this.activeWorkers--;
// 执行回调
if (worker.resolve) {
worker.resolve(result);
worker.resolve = null;
worker.reject = null;
}
// 处理队列中的下一个任务
this.processQueue();
});
worker.on('error', (err) => {
worker.busy = false;
this.activeWorkers--;
if (worker.reject) {
worker.reject(err);
worker.resolve = null;
worker.reject = null;
}
// 重启 Worker
const index = this.workers.indexOf(worker);
if (index !== -1) {
this.workers[index] = this.createWorker();
}
});
return worker;
}
execute(taskData) {
return new Promise((resolve, reject) => {
// 查找空闲 Worker
const idleWorker = this.workers.find((w) => !w.busy);
if (idleWorker) {
idleWorker.busy = true;
idleWorker.resolve = resolve;
idleWorker.reject = reject;
this.activeWorkers++;
idleWorker.postMessage(taskData);
} else {
// 加入队列等待
this.taskQueue.push({ taskData, resolve, reject });
}
});
}
processQueue() {
if (this.taskQueue.length === 0) return;
const idleWorker = this.workers.find((w) => !w.busy);
if (!idleWorker) return;
const { taskData, resolve, reject } = this.taskQueue.shift();
idleWorker.busy = true;
idleWorker.resolve = resolve;
idleWorker.reject = reject;
this.activeWorkers++;
idleWorker.postMessage(taskData);
}
getStats() {
return {
total: this.poolSize,
busy: this.activeWorkers,
idle: this.poolSize - this.activeWorkers,
queued: this.taskQueue.length
};
}
}
// 创建 Worker 池
const pool = new WorkerPool(
path.join(__dirname, 'worker-main.js'),
numCPUs
);
// HTTP 服务器
const server = http.createServer(async (req, res) => {
if (req.url === '/api/fibonacci') {
try {
const result = await pool.execute({ type: 'fibonacci', n: 40 });
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ result, stats: pool.getStats() }));
} catch (err) {
res.writeHead(500);
res.end(JSON.stringify({ error: err.message }));
}
} else if (req.url === '/api/image-process') {
try {
const result = await pool.execute({ type: 'image-process', size: 1000 });
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ result, stats: pool.getStats() }));
} catch (err) {
res.writeHead(500);
res.end(JSON.stringify({ error: err.message }));
}
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ stats: pool.getStats() }));
}
});
server.listen(3000, () => {
console.log(`主线程 ${process.pid} 启动,Worker 池大小: ${numCPUs}`);
});
} else {
// Worker 线程:处理 CPU 密集型任务
parentPort.on('message', (task) => {
let result;
switch (task.type) {
case 'fibonacci':
result = fibonacci(task.n);
break;
case 'image-process':
result = processImage(task.size);
break;
default:
result = { error: 'Unknown task type' };
}
parentPort.postMessage(result);
});
// CPU 密集型:斐波那契计算
function fibonacci(n) {
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b];
}
return b;
}
// CPU 密集型:模拟图像处理
function processImage(size) {
const data = new Float64Array(size * size);
for (let i = 0; i < data.length; i++) {
data[i] = Math.sin(i) * Math.cos(i) * Math.sqrt(i);
}
return { processed: data.length, sample: data[0] };
}
}
💡 **提示:**Worker Threads 最大的优势是可以通过
SharedArrayBuffer在线程间共享内存,避免了进程间通信的数据序列化开销。对于需要频繁交换大数据的场景(如图像处理、数据分析),Worker Threads 比 Cluster 更高效。
3.2 Cluster vs Worker Threads 选择指南
需要处理 HTTP 请求?
├── 是 → 使用 Cluster / PM2
│ ├── 简单场景 → PM2 cluster 模式
│ └── 需要精细控制 → Cluster 模块
└── 否 → 需要 CPU 密集计算?
├── 是 → 使用 Worker Threads
│ ├── 需要共享内存 → SharedArrayBuffer
│ └── 不需要共享 → MessagePort
└── 否 → 使用单进程 + 异步 I/O
🔐 四、生产环境最佳实践
4.1 健康检查与监控
// health-check.js - 完整的健康检查实现
const http = require('node:http');
// 健康状态
const healthStatus = {
isReady: false,
isShuttingDown: false,
checks: {
database: { status: 'unknown', latency: 0 },
redis: { status: 'unknown', latency: 0 },
memory: { status: 'unknown', usage: 0 }
},
startedAt: new Date().toISOString(),
uptime: 0
};
// 启动就绪检查
async function checkReadiness() {
try {
// 检查数据库连接
const dbStart = Date.now();
// await db.raw('SELECT 1');
healthStatus.checks.database = {
status: 'ok',
latency: Date.now() - dbStart
};
// 检查 Redis 连接
const redisStart = Date.now();
// await redis.ping();
healthStatus.checks.redis = {
status: 'ok',
latency: Date.now() - redisStart
};
// 检查内存使用
const memUsage = process.memoryUsage();
const heapUsedPercent = (memUsage.heapUsed / memUsage.heapTotal) * 100;
healthStatus.checks.memory = {
status: heapUsedPercent > 90 ? 'warning' : 'ok',
usage: Math.round(heapUsedPercent),
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024) + 'MB',
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024) + 'MB',
rss: Math.round(memUsage.rss / 1024 / 1024) + 'MB'
};
healthStatus.isReady = true;
healthStatus.uptime = process.uptime();
return true;
} catch (err) {
healthStatus.isReady = false;
return false;
}
}
// HTTP 健康检查端点
function createHealthServer(port = 3001) {
return http.createServer((req, res) => {
// 存活检查(Liveness):进程是否在运行
if (req.url === '/healthz') {
if (healthStatus.isShuttingDown) {
res.writeHead(503);
res.end('Shutting Down');
return;
}
res.writeHead(200);
res.end('OK');
return;
}
// 就绪检查(Readiness):是否可以接收流量
if (req.url === '/readyz') {
if (!healthStatus.isReady || healthStatus.isShuttingDown) {
res.writeHead(503);
res.end('Not Ready');
return;
}
res.writeHead(200);
res.end('Ready');
return;
}
// 详细健康信息
if (req.url === '/health') {
const statusCode = healthStatus.isReady ? 200 : 503;
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(healthStatus, null, 2));
return;
}
res.writeHead(404);
res.end('Not Found');
});
}
module.exports = { checkReadiness, createHealthServer, healthStatus };
⚠️ **警告:**健康检查端点不要放在主应用服务器上!使用独立的 HTTP 服务器(不同端口)来提供健康检查。这样即使主应用卡住,负载均衡器仍然能通过健康检查端点检测到进程存活,及时将流量切走。
4.2 内存泄漏检测与自动重启
// memory-guard.js - 内存泄漏防护
const cluster = require('node:cluster');
function setupMemoryGuard(options = {}) {
const {
maxHeapUsedMB = 512, // 最大堆内存(MB)
checkIntervalMs = 30000, // 检查间隔(ms)
consecutiveThreshold = 3 // 连续超标次数
} = options;
let consecutiveOverLimit = 0;
setInterval(() => {
const memUsage = process.memoryUsage();
const heapUsedMB = memUsage.heapUsed / 1024 / 1024;
if (heapUsedMB > maxHeapUsedMB) {
consecutiveOverLimit++;
console.warn(
`[Memory Guard] PID: ${process.pid}, ` +
`Heap: ${heapUsedMB.toFixed(1)}MB / ${maxHeapUsedMB}MB, ` +
`连续超标: ${consecutiveOverLimit}/${consecutiveThreshold}`
);
if (consecutiveOverLimit >= consecutiveThreshold) {
console.error(
`[Memory Guard] 内存持续超标,Worker ${process.pid} 即将重启`
);
// 通知 PM2 或主进程重启
if (process.send) {
process.send('restart');
}
process.exit(1);
}
} else {
consecutiveOverLimit = 0;
}
}, checkIntervalMs);
}
// 在 Worker 中使用
if (cluster.isWorker) {
setupMemoryGuard({ maxHeapUsedMB: 512 });
}
4.3 生产环境部署清单
在将 Node.js 集群应用部署到生产环境之前,请确认以下清单:
✅ 部署前检查:
- ✅ 使用 PM2 集群模式或容器化方案(Docker + K8s)
- ✅ 配置优雅退出(Graceful Shutdown),处理
SIGTERM信号 - ✅ 设置健康检查端点(
/healthz+/readyz) - ✅ 配置内存限制和自动重启策略(
max_memory_restart) - ✅ 启用日志轮转(
pm2-logrotate或系统logrotate) - ✅ 配置进程重启次数限制(防止无限重启风暴)
- ✅ 使用
pm2 reload而非pm2 restart进行零停机部署 - ✅ 监控 Worker 进程状态(CPU、内存、事件循环延迟)
❌ 常见错误:
- ❌ 在生产环境使用
node app.js直接运行(无进程守护) - ❌ 忘记处理
uncaughtException和unhandledRejection - ❌ 在 Cluster 模式下使用文件锁或本地缓存(各 Worker 互不可见)
- ❌ 健康检查放在主应用路由中(主应用卡死时检查也卡死)
- ❌ 不限制 Worker 重启次数(内存泄漏导致无限重启)
💡 总结
Node.js 的多进程和多线程方案各有适用场景。对于大多数 Web 服务器应用,PM2 集群模式是最实用的选择——它开箱即用,提供了进程管理、零停机部署、日志管理和监控等生产必需功能。如果你需要更精细的控制,可以直接使用 Cluster 模块。对于 CPU 密集型计算任务,Worker Threads 是更好的选择,它通过线程间共享内存避免了进程间通信的开销。
**⚡ 关键结论:**不要在生产环境跑单进程 Node.js。即使是最低配置的 2 核服务器,启用 Cluster 模式也能让你的吞吐量翻倍。而 PM2 让这一切变得简单到只需要一行命令:pm2 start ecosystem.config.js --env production。
🔧 相关工具推荐:
- PM2 — Node.js 生产级进程管理器
- node:cluster — Node.js 内置集群模块
- node:worker_threads — Node.js 内置 Worker Threads
- Clinic.js — Node.js 性能诊断工具
- 0x — 火焰图生成工具
- Prometheus + Grafana — 进程监控和可视化