Node多进程管理

我们都知道, JavaScript 代码是运行在单个进程的单线程上。它存在以下问题:

  1. 一旦程序抛出的异常没有被捕获,将会引起整个进程的崩溃。
  2. 无法充分利用服务器的多核 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_processnet 模块实现
  • 多个工作线程可以共享同一端口
  • 内置负载均衡(轮询调度,每次选择第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选项复用端口,仅某些平台可用)
管理功能 ❌(需要编码处理故障重启,日志采集等等) ❌(需要编码处理故障重启,日志采集等等) ✅(进程守护、日志系统、部署等)
适用场景 执行命令/脚本 多核服务部署 生产环境部署,守护多实例服务
相关推荐
问道飞鱼9 分钟前
【Springboot知识】Springboot计划任务Schedule详解
java·spring boot·后端·schedule
o0o0o0D2 小时前
jmeter 执行顺序和组件作用域
后端
神仙别闹2 小时前
基于ASP.NET+MySQL实现待办任务清单系统
后端·mysql·asp.net
程序员buddha2 小时前
【Spring Boot】Spring Boot + Thymeleaf搭建mvc项目
spring boot·后端·mvc
okok__TXF3 小时前
spring详解-循环依赖的解决
java·后端·spring
二十雨辰5 小时前
[学成在线]23-面试题总结
java·后端
神马都会亿点点的毛毛张5 小时前
【SpringBoot教程】SpringBoot自定义注解与AOP实现切面日志
java·spring boot·后端·spring·spring aop·aspectj
[email protected]6 小时前
ASP.NET Core 中间件
后端·中间件·asp.net·.netcore
不懂英语的程序猿7 小时前
【SF顺丰】顺丰开放平台API对接(Java对接篇)
前端·后端
muyouking118 小时前
Rust中避免过度使用锁导致性能问题的策略
开发语言·后端·rust