我们都知道, JavaScript 代码是运行在单个进程的单线程上。它存在以下问题:
- 一旦程序抛出的异常没有被捕获,将会引起整个进程的崩溃。
- 无法充分利用服务器的多核 CPU 。
面对单进程单线程带来的问题,我们的经验是启动多个进程来解决。
使用 child_process 内置模块
child_process
模块允许你创建子进程来执行系统命令或 Node 脚本。
特点:
- 可以执行系统命令或者 Node 脚本。
- 提供四种创建子进程的方式:
spawn()
: 启动一个子进程执行命令exec()
: 启动一个子进程来执行命令并在缓冲区中返回结果execFile()
: 启动一个子进程来执行可执行文件fork()
: 特殊的 spawn() 用于创建 Node 子进程
使用场景:
- 执行耗时操作,避免阻塞事件循环(更建议使用
worker_threads
模块) - 执行系统命令或脚本
- 启动多个 Node 实例,如 Web 服务器
示例:
主进程的代码保存为 master.js
javascript
const cp = require('node:child_process')
const os = require('node:os')
const net = require('node:net')
const workers = {}
// 最大启动次数
const maxRetry = 10
// 启动首次启动与最后一次启动时间间隔
const maxRetryInterval = 60 * 1000
let restartTimeList = []
const server = net.createServer().listen(8080)
function createWorker() {
if (isRestartFrequently()) {
throw new Error('Workers frequently restart!')
}
const worker = cp.fork('./worker.js')
worker.on('message', (message) => {
if (message.action === 'restart') {
console.error(`Worker ${worker.pid} restarting...`)
createWorker()
}
})
worker.on('exit', (code) => {
console.error(`Worker ${worker.pid} Exiting with code: ${code}.`)
workers[worker.pid] = null
})
worker.send('server', server)
workers[worker.pid] = worker
console.info(`Worker ${worker.pid} started.`)
}
// 检查是否频繁启动
function isRestartFrequently() {
const length = restartTimeList.push(Date.now())
// 保留最后10次启动记录
if (length > maxRetry) {
restartTimeList = restartTimeList.slice(maxRetry * -1)
}
return restartTimeList.length >= maxRetry && restartTimeList[restartTimeList.length - 1] - restartTimeList[0] < maxRetryInterval
}
os.cpus().forEach(createWorker)
// 主进程退出,退出所有子进程
process.on('exit', (code) => {
console.error(`Master Exiting with code: ${code}.`)
for (const pid in workers) {
workers[pid].kill()
}
})
工作进程的代码保存为 worker.js
javascript
const http = require('node:http')
const _ = require('lodash')
let worker
const server = http.createServer((req, res) => {
if (_.random(0, 5) === 1) {
throw new Error('abort!!!!')
}
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({
message: `Hello, I am worker ${process.pid}`,
}))
})
process.on('message', (message, tcp) => {
if (message === 'server') {
worker = tcp
worker.on('connection', (socket) => {
server.emit('connection', socket)
})
}
})
process.on('uncaughtException', (err) => {
console.error(`Worker ${process.id} Exiting with error: ${err.message}.`)
process.send({
action: 'restart',
})
worker.close(() => process.exit(1))
})
使用 cluster 内置模块
基于 child_process 实现的集群管理,它允许轻松创建所有共享服务器端口的子进程。
特点:
- 基于
child_process
和net
模块实现 - 多个工作线程可以共享同一端口
- 内置负载均衡(轮询调度,每次选择第
i=(i+1) mod N
个进程来发送连接)
使用场景:
- 充分利用多核 CPU 资源
- 提供应用可用性
示例:
主进程的代码保存为 master.js
js
const cluster = require('node:cluster')
const os = require('node:os')
const workers = {}
function createWorker() {
const worker = cluster.fork()
worker.on('message', (message) => {
if (message.action === 'restart') {
console.error(`Worker ${worker.process.pid} restarting...`)
createWorker()
}
})
worker.on('exit', (code) => {
console.error(`Worker ${worker.process.pid} exiting with code: ${code}.`)
workers[worker.id] = null
})
workers[worker.id] = worker
}
cluster.setupPrimary({
exec: 'worker.js',
})
os.cpus().forEach(createWorker)
// 主进程退出,退出所有子进程
process.on('exit', (code) => {
console.error(`Master Exiting with code: ${code}.`)
for (const id in workers) {
workers[id].kill()
}
})
工作进程的代码保存为 worker.js
js
const http = require('node:http')
const _ = require('lodash')
const server = http.createServer((req, res) => {
if (_.random(0, 5) === 1) {
throw new Error('abort!!!!')
}
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({
message: `Hello, I am worker ${process.pid}`,
}))
}).listen(8080, () => {
console.info(`Worker ${process.pid} is running at http://localhost:8080`)
})
process.on('uncaughtException', (err) => {
console.error(`Worker ${process.pid} Exiting with error: ${err.message}.`)
process.send({
action: 'restart',
})
server.close(() => process.exit(1))
})
使用 pm2 第三方模块
pm2 是一个流行的 Node 进程管理器,具备负责均衡、日志管理、监控等功能。
特点:
- 进程管理
- 负责均衡
- 日志管理
- 监控功能
- 配置文件管理多应用
使用场景:
- 生产环境部署 Node 中小型应用
- 需要高可用性和自动恢复场景
- 需要监控和管理多个 Node 应用场景
示例:
配置文件代码保存为 ecosystem.config.js
javascript
module.exports = {
apps : [{
name: 'myApp',
script: 'app.js',
exec_mode: 'cluster', // 使用集群模式
max_restarts: 10, // 最大重启次数
restart_delay: 0,
instances: 'max', // 根据CPU核心数量启动多个进程
}],
};
应用代码保存为 app.js
javascript
const http = require('node:http')
const _ = require('lodash')
http.createServer((req, res) => {
if (_.random(0, 5) === 1) {
throw new Error('abort!!!!')
}
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({
message: `Hello, I am worker ${process.pid}`,
}))
}).listen(8080, () => {
console.info(`Server is running at http://localhost:8080`)
})
基本用法:
bash
# 启动应用
pm2 start app.js
# 通过配置文件启动应用
pm2 start ecosystem.config.js
# 显示所有进程状态
pm2 list
# 删除特定进程
pm2 delete pid
# 停止特定进程
pm2 stop pid
# 重启特定进程
pm2 restart pid
# 扩容,动态增加2个实例
pm2 scale myApp +2
# 将实例增加到2个或减少为2个
pm2 scale myApp 2
# 查看日志
pm2 logs
# 监控
pm2 monit
总结
特性 | child_process | cluster | pm2 |
---|---|---|---|
是否支持多进程 | ✅ | ✅ | ✅ |
是否共享端口 | ❌(可以通过设置listen() 的reusePort 选项复用端口,仅某些平台可用) |
✅ | ✅ |
管理功能 | ❌(需要编码处理故障重启,日志采集等等) | ❌(需要编码处理故障重启,日志采集等等) | ✅(进程守护、日志系统、部署等) |
适用场景 | 执行命令/脚本 | 多核服务部署 | 生产环境部署,守护多实例服务 |