
文章目录1. 引言2. PM2 是什么一句定义PM2 的 7 大核心能力PM2 适合谁3. 安装与第一个应用3.1 安装 PM23.2 准备一个测试应用3.3 用 PM2 启动它3.4 理解 pm2 list 的输出4. 核心功能详解4.1 进程守护与自动重启4.2 常用管理命令速查4.3 日志管理4.4 集群模式榨干多核性能4.5 开机自启5. 配置文件生态Ecosystem File5.1 为什么需要配置文件5.2 生成模板5.3 配置文件结构详解5.4 用配置文件启动6. 实战部署一个 Express 应用6.1 项目准备6.2 启动与验证6.3 模拟崩溃与自愈6.4 零停机更新7. 常见问题与踩坑Q1pm2 start 后提示端口被占用Q2环境变量不生效Q3升级 Node.js 后 PM2 应用挂了Q4在 Docker 容器里还要不要用 PM28. 总结参考1. 引言你大概率遇到过这样的场景写完一个 Node.js 服务SSH 连上服务器敲下node app.js一切正常。然后你关掉终端下班回家第二天发现服务挂了——因为终端退出时进程也跟着死了。或者更糟的凌晨三点服务因为一个未捕获的异常崩了没人知道直到用户投诉才慌忙重启。这些问题的本质是一样的直接用node命令启动的服务是裸奔的——没有守护、没有自愈、没有监控。就像让一个没有社保、没有医保的员工直接上岗出了事只能靠天命。PM2 就是来解决这个问题的。它负责一件很朴素的事让你的 Node.js 应用始终在线挂了就拉起来日志帮你记好还能充分利用多核 CPU。读完这篇文章你将掌握PM2 是什么以及它解决了什么问题用 PM2 启动、停止、重启、监控 Node.js 应用配置集群模式榨干多核性能让应用在服务器重启后自动启动用一个真实的 Express 项目完成从零到生产的部署2. PM2 是什么一句定义PM2Process Manager 2是一个Node.js 生产环境进程管理器。它由 Keymetrics 团队维护在 GitHub 上有超过 40k Star是 Node.js 生态中事实上的进程管理标准。GitHub地址GitHub - Unitech/pm2: Node.js Production Process Manager with a built-in Load Balancer.它本质上是一个跑在后台的守护进程Daemon帮你看着所有应用进程——谁挂了就重启谁谁出错了就把日志记下来谁来请求了就帮你在多个进程间分配。PM2 的 7 大核心能力能力一句话说明进程守护应用崩溃自动重启不用你半夜爬起来救火负载均衡启动多个进程实例自动把请求分给不同实例日志管理自动收集 stdout/stderr支持日志轮转防止硬盘写满零停机重载pm2 reload逐个重启进程更新代码时服务不中断开机自启服务器重启后自动拉起所有应用实时监控终端仪表盘CPU、内存、请求量一目了然配置文件驱动一份ecosystem.config.js描述所有应用适合团队和 CI/CDPM2 适合谁你不需要是一个运维老手才能用 PM2。只要你的工作涉及以下任意一种场景PM2 就对你有用在云服务器上部署 Node.js 后端服务跑一个 Nuxt/Next 的全栈应用管理多个微服务进程需要一个比nohup 靠谱的后台运行方案想在单台机器上充分利用多核 CPU守护进程 PM2 Daemon 在中央管理着多个应用进程app1、app2…收集日志到统一位置通过 pm2 命令行工具进行交互3. 安装与第一个应用3.1 安装 PM2PM2 是一个 npm 全局包一行命令搞定npminstall-gpm2安装完成后验证一下pm2--version# 输出类似5.4.23.2 准备一个测试应用我们先写一个最简 HTTP 服务来玩。创建文件app.jsconsthttprequire(http);constserverhttp.createServer((req,res){res.writeHead(200,{Content-Type:application/json});res.end(JSON.stringify({message:Hello from PM2!,pid:process.pid,// 进程ID后面讲集群模式时会很有趣time:newDate().toISOString()}));});server.listen(3000,(){console.log(Server running on port 3000, PID:${process.pid});});3.3 用 PM2 启动它pm2 start app.js--namemy-app--name my-app给应用取个名字后续管理时比用数字 ID 方便得多。启动后你应该看到类似输出[PM2] Starting /home/user/app.js in fork_mode (1 instance) [PM2] Done. ┌─────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐ │ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │ ├─────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤ │ 0 │ my-app │ default │ 1.0.0 │ fork │ 12345 │ 0s │ 0 │ online │ 0% │ 25.3mb │ user │ disabled │ └─────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘如图我启动的server服务现在用浏览器或 curl 访问http://localhost:3000你会看到 JSON 响应。更有意思的是关掉终端再打开一个新终端访问http://localhost:3000服务还在。这就是 PM2 守护进程在起作用——它不是挂在你那个终端会话下面的而是被 PM2 Daemon 接管了。3.4 理解pm2 list的输出用pm2 list随时查看所有应用的状态。关键列含义列名含义idPM2 内部的进程编号name你给应用起的名字modefork单进程或cluster集群模式pid操作系统级别的进程 IDstatusonline正常/stopped停止/errored异常↺重启次数——这个数字如果疯涨说明代码有问题uptime运行时长刚启动显示0scpu / mem当前 CPU 和内存占用实用技巧如果你看到某个应用的重启次数↺在快速上涨说明它反复崩溃——赶紧去看日志。4. 核心功能详解4.1 进程守护与自动重启这可能是 PM2 最重要的功能崩溃自愈。试试故意让我们的应用崩溃。修改app.js加一个定时炸弹consthttprequire(http);constserverhttp.createServer((req,res){// 每处理 5 个请求就模拟一次崩溃if(Math.random()0.2){process.exit(1);// 模拟致命错误}res.writeHead(200,{Content-Type:application/json});res.end(JSON.stringify({status:ok}));});server.listen(3000);用 PM2 重启应用后连续发几次请求pm2 restart my-app# 连续发 10 个请求foriin{1..10};docurl-shttp://localhost:3000;echo;done你会发现前几个请求还能正常响应随机到process.exit(1)时服务瞬间挂掉——但下一秒又好了。这就是 PM2 在检测到进程退出后立刻重新拉起。用pm2 list看一眼↺列的数字涨了。防止无限重启如果代码写了死循环的 bugPM2 不会无限重启下去。它默认的机制是如果 15 秒内重启超过 15 次就放弃把进程标为errored。你可以通过参数调整pm2 start app.js --max-restarts5--restart-delay3000# 最多重启5次每次间隔至少3秒4.2 常用管理命令速查所有管理命令的入口都是pm2 action app-name-or-id# 启动pm2 start app.js--namemy-app# 指定名称启动pm2 start app.js-i2--nameapi# 集群模式启动2个实例pm2 start ecosystem.config.js# 用配置文件启动推荐# 查看pm2 list# 应用列表最常用pm2 show my-app# 单个应用详情路径、日志位置、环境变量pm2 monit# 实时仪表盘——CPU、内存、请求量都在滚动# 控制pm2 stop my-app# 停止保留在 PM2 列表中pm2 restart my-app# 重启pm2 delete my-app# 从 PM2 列表中彻底移除pm2 reload my-app# 零停机重载逐个重启实例集群模式下特别有用# 全局pm2kill# 杀掉 PM2 Daemon 本身所有应用全部停止pm2 flush# 清空所有日志pm2 save# 保存当前进程列表下次 PM2 启动时自动恢复restartvsreload的区别很重要restart是直接全部杀掉再启动会短暂中断reload是一个一个重启服务不中断。生产环境更新代码时用reload。pm2 monit 实时仪表盘如下4.3 日志管理PM2 自动接管了应用的标准输出stdout和标准错误stderr存到统一位置pm2 logs# 实时查看所有应用日志tail -f 的效果pm2 logs my-app# 只看指定应用pm2 logs--lines200# 看最近200行pm2 logs--err# 只看错误日志日志文件默认位置在~/.pm2/logs/~/.pm2/logs/ ├── my-app-out.log # 标准输出日志 ├── my-app-error.log # 错误日志 └── ...日志轮转线上跑久了日志文件会变得巨大。安装 PM2 日志轮转模块pm2installpm2-logrotate# 配置可选pm2setpm2-logrotate:max_size 10M# 单个日志文件最大 10MBpm2setpm2-logrotate:retain30# 保留最近 30 个日志文件pm2setpm2-logrotate:compresstrue# 压缩旧日志一旦日志文件超过max_sizePM2 会自动切分、归档旧的会自动删除——不用担心硬盘被日志写满。4.4 集群模式榨干多核性能Node.js 默认是单线程的一个进程只能跑在一个 CPU 核上。如果你的服务器有 4 核只开一个 Node 进程意味着另外 3 个核在摸鱼。PM2 的集群模式Cluster Mode就是答案。它在底层使用了 Node.js 原生的cluster模块自动 fork 出多个工作进程并把请求分发给它们# 启动与 CPU 核数相等的进程pm2 start app.js-imax# 或者指定数量pm2 start app.js-i4再看看pm2 list你会发现同一个应用出现了多个进程实例mode列显示cluster。注意集群模式要求应用是无状态的或共享状态。如果应用依赖内存中的本地变量非数据库/Redis不同请求打到不同的工作进程时会拿到不同的数据。生产环境建议用 Redis 或数据库来共享状态。cluster和fork模式的对比维度fork 模式cluster 模式进程数1 个N 个通常等于 CPU 核数CPU 利用单核多核负载均衡无PM2 内置适用场景开发调试、简单脚本、cron job生产环境 HTTP 服务端口直接监听多进程共享同一端口4.5 开机自启服务器总会因为各种原因重启——安全补丁、机房维护、或是电源闪断。你不可能每次重启都手动 SSH 上去pm2 start一遍。PM2 的开机自启方案很优雅一条命令搞定# 1. 生成并配置启动脚本会根据你的系统是 systemd/upstart/launchd 自动适配pm2 startup# 上面命令会输出一条 sudo 命令复制执行它# 示例输出sudo env PATH$PATH:/usr/bin pm2 startup systemd -u youruser --hp /home/youruser# 2. 把当前运行的应用列表保存为需要开机恢复的快照pm2 save此后每次服务器重启PM2 会自动启动然后根据pm2 save存的快照把所有应用拉起来。验证是否配置成功以 systemd 为例systemctl status pm2-$(whoami)# 或systemctl status pm2-root5. 配置文件生态Ecosystem File5.1 为什么需要配置文件前面所有操作都靠命令行传参。这在探索阶段没问题但生产环境不行不可复现换个服务器还得重新敲一遍命令忘了某个参数服务就跑得不对团队协作差同事不知道你启动时传了什么参数CI/CD 不友好自动化脚本拼命令行参数又丑又脆多环境无法管理开发、测试、生产环境参数不同命令行根本管不过来PM2 的答案是Ecosystem File——一个 JavaScript 配置文件完整描述你的所有应用和运行参数。5.2 生成模板pm2 ecosystem这条命令会在当前目录生成一个ecosystem.config.js模板文件。PM2 5.x 默认生成 ESM 格式export default如果你需要旧的 CommonJS 格式module.exports用pm2 ecosystem--cjs5.3 配置文件结构详解下面是一个生产级的ecosystem.config.js示例部署一个 Express API 服务module.exports{apps:[{// ---- 基本 ----name:api-server,// 应用名称pm2 list 中显示script:./src/server.js,// 入口文件cwd:/var/www/api-server,// 工作目录// ---- 集群 ----instances:max,// max CPU核数或指定数字如 2exec_mode:cluster,// fork 或 cluster// ---- 环境变量 ----env:{NODE_ENV:development,PORT:3000,},env_production:{NODE_ENV:production,PORT:8080,},env_staging:{NODE_ENV:staging,PORT:3001,},// ---- 日志 ----error_file:./logs/err.log,// 错误日志路径out_file:./logs/out.log,// 输出日志路径log_date_format:YYYY-MM-DD HH:mm:ss Z,// 日志时间戳格式merge_logs:true,// 集群模式下合并所有实例的日志// ---- 重启策略 ----max_restarts:10,// 最多重启次数restart_delay:2000,// 两次重启的最小间隔(ms)min_uptime:10s,// 至少运行10秒才算启动成功// ——防止有bug的代码在无限快速重启循环max_memory_restart:500M,// 内存超过500M自动重启// ---- 更新 ----watch:false,// 生产环境关掉文件监听ignore_watch:[node_modules,logs],// ---- 其他 ----autorestart:true,// 崩溃自动重启kill_timeout:5000,// 强杀前的等待时间(ms)}],// 部署配置可选用于 pm2 deploydeploy:{production:{user:deploy,host:192.168.1.100,ref:origin/main,repo:gitgithub.com:user/api-server.git,path:/var/www/production,post-deploy:npm install pm2 reload ecosystem.config.js --env production,},},};5.4 用配置文件启动# 默认环境envpm2 start ecosystem.config.js# 指定生产环境使用 env_productionpm2 start ecosystem.config.js--envproduction# 重载生产环境零停机更新pm2 reload ecosystem.config.js--envproduction有了配置文件整条部署链路就通顺了拉代码 → 装依赖 →pm2 reload——一行搞定服务不中断。6. 实战部署一个 Express 应用让我们把前面学的串起来完成一次完整的 PM2 部署流程。6.1 项目准备项目目录结构express-api/ ├── src/ │ └── server.js ├── package.json └── ecosystem.config.jsserver.js——一个简单的 Express API返回当前时间和服务的进程 PIDconstexpressrequire(express);constappexpress();// 一个简单的监控端点app.get(/health,(req,res){res.json({status:ok});});// 主业务接口app.get(/api/time,(req,res){res.json({time:newDate().toISOString(),pid:process.pid,// 可以看到请求被分发到了哪个工作进程env:process.env.NODE_ENV||development,});});constPORTprocess.env.PORT||3000;app.listen(PORT,(){console.log(Server started on port${PORT}, PID:${process.pid});});ecosystem.config.jsmodule.exports{apps:[{name:express-api,script:./src/server.js,instances:2,// 启动2个实例exec_mode:cluster,env:{NODE_ENV:development,PORT:3000,},env_production:{NODE_ENV:production,PORT:8080,},max_restarts:5,min_uptime:5s,error_file:./logs/err.log,out_file:./logs/out.log,log_date_format:YYYY-MM-DD HH:mm:ss,merge_logs:true,}],};6.2 启动与验证# 安装依赖npminstall# 用 PM2 启动pm2 start ecosystem.config.js# 查看状态——应该看到两个 online 的进程pm2 list验证服务# 多请求几次观察 pid 的变化——你会发现 pid 在两个值之间轮换curlhttp://localhost:3000/api/timecurlhttp://localhost:3000/api/timecurlhttp://localhost:3000/api/time这说明 PM2 的负载均衡在正常工作——请求被轮流分发给两个工作进程。6.3 模拟崩溃与自愈# 查看当前两个进程的 PIDpm2 list# 手动杀掉其中一个进程kill 它的 pidkillpid# 再看一眼——被杀的那个进程 pid 变了被重启了↺ 次数 1pm2 list6.4 零停机更新假设你改了代码需要更新线上服务gitpullnpminstallpm2 reload ecosystem.config.js--envproductionreload会逐个重启实例先起一个新实例等它就绪后再杀掉一个旧实例循环直到全部替换。整个过程中剩余实例继续处理请求用户无感知。7. 常见问题与踩坑Q1pm2 start后提示端口被占用大概率是上一个 PM2 实例已经运行了但你没注意到。先用pm2 list确认不要同时用node和pm2启动同一端口的服务。如果确认端口被其他程序占用# Windowsnetstat-ano|findstr :3000# Linux / macOSlsof-i:3000Q2环境变量不生效两个常见陷阱写错位置了。env_xxx的xxx需要和--env xxx精确匹配。env_production对应--env production不是--env prod。在app.js中console.log(process.env)没看到变量。在 ecosystem 中环境变量只对入口脚本及其子进程生效你在 ecosystem 外面先export再pm2 start是无效的。正确姿势是先在配置文件的env中声明然后用--env指定环境。Q3升级 Node.js 后 PM2 应用挂了PM2 保存的是 Node.js 的绝对路径。你升级 Node.js比如用 nvm 切版本路径变了PM2 还在找旧的 Node 二进制文件自然启动不了。解决方案# 先删掉旧进程pm2 delete all# 重新生成启动脚本pm2 unstartup# 清理旧的pm2 startup# 用新 Node 路径重新生成# 重新启动应用pm2 start ecosystem.config.js pm2 saveQ4在 Docker 容器里还要不要用 PM2这是个经典问题答案取决于场景场景建议单容器单进程不用 PM2让 Docker 本身做进程管理和重启--restart always单容器需要多核利用用 PM2 cluster 模式在容器内 fork 多进程单容器跑多个不同服务可以但不推荐——Docker 哲学是一容器一进程如果决定在容器里用 PM2记得以pm2-runtime而不是pm2作为入口它专为容器设计不会后台化CMD [pm2-runtime, start, ecosystem.config.js]一个判断标准如果你需要在一个容器里跑多个 Node 实例来利用多核PM2 cluster 是正确的选择。如果只需要单进程Docker 自带的 restart 策略就够了。8. 总结PM2 的核心价值可以浓缩为四个字守护、集群、日志、自启。能力一命令速查启动并守护pm2 start app.js --name xxx查看状态pm2 list/pm2 monit集群多核pm2 start app.js -i max查看日志pm2 logs xxx零停机更新pm2 reload xxx开机自启pm2 startup→pm2 save配置文件pm2 start ecosystem.config.js --env production参考PM2 - 主页 - PM2 进程管理器https://juejin.cn/post/7444450350190346278https://pm2.fenxianglu.cn/docs/start