ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PM2完全指南:从进程守护到集群模式与部署实战

PM2完全指南:从进程守护到集群模式与部署实战 1. 为什么还要单独聊 PM2它究竟解决了什么问题早年我还在用node app.js直跑服务的时候踩过不少坑。最难受的不是代码本身报错而是进程挂了没人知道。本地开发还好顶多重启一下线上环境一旦服务进程异常退出用户那边立刻就是“网站打不开”等你发现的时候日志已经刷了好几屏。那时候的常规操作是nohup node app.js 配合 crontab 定时检查端口流程繁琐不说还会遇到端口释放慢、进程僵死、日志被撑爆等一系列问题。后来切到 PM2 之后服务的稳定性明显上了一个台阶。这篇文章不是纯文档翻译而是把我实际使用 PM2 的经验、配置习惯和踩过的坑梳理一遍给刚接触 Node.js 进程管理的朋友一个可以直接上手的路线。PM2Process Manager 2是 Node.js 生态里最主流的进程管理工具。它的核心能力围绕三点展开守护进程、自动重启、日志管理。往深了说它还能做集群模式负载均衡、零停机部署、内存监控、开机自启等。对于中小型项目的线上运维PM2 几乎是一站式解决方案。适合谁来读这篇文章首先是刚把 Node.js 服务部署到云服务器上的新手其次是正在为“进程老是崩溃却不自知”而头疼的团队再就是想要把部署流程标准化、减少手工操作的运维同学。即便你暂时没到线上部署阶段提前了解 PM2 的思路也会对理解 Node.js 应用的生命周期管理有帮助。2. PM2 的核心机制与设计思路2.1 进程守护到底守护的是什么很多初学者会把“守护”理解成“保证进程不挂”这个说法不够准确。PM2 做的事情其实更实际它监控进程的运行状态当进程异常退出时立即拉起一个新的进程并且通过 God Daemon 这个常驻后台服务来维持对子进程的控制权。这里有一个关键的设计思路值得展开。Node.js 默认是单线程模型任何未捕获的异常都可能导致进程直接崩溃。如果没有外部守护者崩溃之后就只能在系统层面通过 systemd 或 supervisord 这类工具来拉起。PM2 相当于在应用层和系统层之间加了一层轻量级的代理它本身以 daemon 形式常驻由它 spawn 出来的子进程才是你的 Node.js 应用。子进程挂掉之后PM2 的 God Daemon 会捕获到退出事件然后按照预设的重启策略重新拉起。这个机制带来的实际价值是你不用关心进程是怎么死的只需要关心重启策略是否合理。比如瞬时崩溃导致无限重启PM2 有max_restarts和min_uptime的配合来防止“死循环式”重启这一点后面在配置优化部分会详细展开。2.2 为什么不用 systemd 或 Docker而是 PM2有些朋友会问既然系统自带 systemd为什么还要用 PM2这个问题我纠结过很久。实际对比下来两者的定位其实是不同的systemd 更适合系统级服务的托管比如数据库、Nginx 这类基础组件而 PM2 更适合应用进程的精细化管理尤其是 Node.js 这种频繁重启、多实例、需要实时查看日志的场景。PM2 相比 systemd 的明显优势在于开发体验。pm2 logs可以实时流式查看日志pm2 monit可以可视化监控 CPU 和内存pm2 reload可以实现秒级重启这些能力在 systemd 里要么配置繁琐要么没有对应的便捷命令。对于小团队快速迭代、频繁发版的使用场景来说PM2 的上手成本和维护成本更低。顺便说一句 Docker 的问题。Docker 解决的是环境隔离和交付一致性的问题PM2 解决的是进程管理和高可用的问题两者并不冲突。实际上很多人会在 Docker 容器里面装 PM2 来管理单个容器内的多进程这种用法也完全合理。建议不必把工具对立起来关键是看你当下的场景缺的是什么。2.3 PM2 与传统 Node.js 直跑的运维痛点对比为了把问题说得更清楚我整理了一张对比表运维维度node app.js直跑nohup crontab 方案PM2崩溃自动重启不支持需要脚本检测原生支持可配置策略日志管理无需手动重定向自动按日期分割、流式查看多实例负载均衡不支持需手动端口分配内置 cluster 模式状态监控无无monit 可视化面板开机自启不支持需手动配置 rc.local一条命令生成 systemd 服务环境变量管理手工设置手工设置配置文件统一管理从这张表能看出来PM2 解决的问题不是某一两个点而是把整个进程生命周期管理的流程打通了。这也是为什么我建议即使是单机单实例的项目也优先用 PM2而不是裸跑 node 进程。3. 从安装到上手PM2 最常用的核心操作3.1 安装与初始配置安装 PM2 没什么好说的全局安装即可npm install -g pm2安装完成后建议先跑一下pm2 --version确认版本。这里有个小细节PM2 版本迭代较快不同大版本之间的命令差异极小但ecosystem.config.js的配置项有细微变化遇到问题先确认版本不要盲目照搬老教程。如果你在服务器上有多个 Node.js 版本比如用 nvm 管理建议在安装后执行pm2 startup生成开机自启服务时确认使用的是哪个 Node 路径。PM2 在生成自启脚本时会锁定当前的 Node 路径如果之后切换了 Node 版本而没有重新执行pm2 startup重启服务器后可能会出现“找不到命令”的情况。3.2 启动项目的几种方式与参数对比PM2 启动一个项目最基础的命令是pm2 start app.js如果你用的是 Express 这类框架入口文件可能是bin/www那也一样直接启动pm2 start bin/www也可以给进程命名便于在进程列表里识别pm2 start app.js --name my-api当你需要在启动时传入 Node.js 运行时参数或者应用本身读取的环境变量可以这样做pm2 start app.js --name my-api --node-args--max-old-space-size2048 --env production这里的--node-args用于指定 V8 引擎的调优参数最典型的就是--max-old-space-size来控制堆内存上限。默认情况下 Node.js 在 64 位系统上大约只能使用 1.5GB 的堆内存如果你的服务需要更多内存而内存条本身够大不加这个参数就会出现“明明内存有 8GB进程却 OOM”的怪象。--env production则会将 PM2 进程的NODE_ENV设置为production。需要注意的是--env只对配置文件中env_xxx字段有效如果直接用命令行启动你需要改为NODE_ENVproduction pm2 start app.js的方式。3.3 常用运维命令速查掌握下面的命令就足够覆盖日常 95% 的使用场景了pm2 list # 查看所有进程的状态 pm2 logs [进程名] # 实时查看日志 pm2 monit # 进入监控面板实时看 CPU 和内存 pm2 restart id|name # 重启某个进程 pm2 reload id|name # 优雅重启零 downtime pm2 stop id|name # 停止进程但保留在列表里 pm2 delete id|name # 删除进程从列表移除 pm2 save # 将当前进程列表存为快照 pm2 resurrect # 恢复上次保存的快照这里重点说一下restart和reload的区别很多人容易混淆。restart是粗暴地杀掉进程再重新拉起期间会有一小段请求失败的空窗期reload走的是 Cluster 模式下的无缝重启流程先启动新的 worker再逐步杀掉旧的 worker整个过程用户感知不到服务中断。但reload有一个前提条件当前进程必须运行在 cluster 模式下exec_mode为cluster并且你启动了至少两个实例。如果只有一个实例reload的效果等同于restart该断的请求还是会断。3.4 查看日志的正确姿势日志是排查线上问题的第一抓手PM2 的日志系统设计得相当顺手。默认情况下PM2 会把标准输出和标准错误流分别写入两个文件文件路径可以用下面的命令查pm2 describe my-api在输出结果里找到out log path和error log path两条就是你要的东西。开发调试时用流式日志更直观pm2 logs my-api这个命令会同时输出 stdout 和 stderr还带颜色区分并且会实时刷新。如果你只想看错误日志可以pm2 logs my-api --err只看标准输出pm2 logs my-api --out还有一个容易被忽略的实用功能pm2 logs --lines 200可以指定显示最近多少行默认是 100 行。看历史日志的时候如果不加这个参数往往只能看到最近一小段内容。4. ecosystem.config.js 配置文件详解4.1 为什么要用配置文件而不是命令行前面讲的都是命令行直接启动这种方式适合快速验证和临时管理。当你的项目逐渐稳定、需要进入标准部署流程时命令行方式就暴露出两个问题一是参数多了记不住二是没办法把配置版本化管理起来。解决方案是ecosystem.config.js。这个文件名其实还可以是ecosystem.config.cjs或ecosystem.json但.js格式最灵活因为里面可以写逻辑、引用环境变量、动态生成配置项。生成配置文件的最快方式pm2 init生成的文件是模板下面是一个我实际在用的完整配置带注释说明每一项的用途module.exports { apps: [ { name: my-api, script: ./src/app.js, args: --port3000, instances: 2, exec_mode: cluster, watch: false, max_memory_restart: 512M, restart_delay: 3000, max_restarts: 10, min_uptime: 5s, exp_backoff_restart_delay: 100, ignore_watch: [node_modules, logs], env: { NODE_ENV: development }, env_production: { NODE_ENV: production }, merge_logs: true, log_date_format: YYYY-MM-DD HH:mm:ss, error_file: ./logs/error.log, out_file: ./logs/out.log } ] };4.2 高频配置项逐一拆解上面这个配置里有几个字段值得展开说明因为它们直接决定了 PM2 行为是否符合预期。instances与exec_mode是负载均衡的关键。exec_mode有两种取值fork和cluster。fork 模式是默认值一个进程实例对应一个 Node.js 进程多开时需要手动管理端口cluster 模式下PM2 会自动创建多个 worker 进程共享同一个端口由 Node.js 内置的 cluster 模块负责请求分发。instances可以写数字或字符串。写2就是固定开两个实例写-1表示实例数等于 CPU 核心数减 1写max表示实例数等于 CPU 核心数。最省心的写法是max但要注意如果你的机器是 2 核那么max就是 2 个实例如果机器本身配置较低比如 1 核max就等于 1此时 cluster 模式的意义就不大了。max_memory_restart是内存超标自动重启的阈值。这个字段的单位可以是K、M、G我习惯写成512M或1G。如果是内存泄漏类问题这个设置能保证服务不会因为一个 worker 吞掉全部内存而拖垮整台机器。restart_delay是重启间隔时间单位毫秒。有些场景下如果进程崩溃后瞬间被拉起可能因为环境还没准备好比如数据库连接池还没释放而再次崩溃形成高频重启风暴。设置一个合理的延迟能有效缓解这个问题。max_restarts与min_uptime这对组合用于防止“假活”。min_uptime表示进程至少稳定运行多少时间才认为它是“健康”的默认值是 1000ms。如果进程启动后不到min_uptime就退出PM2 会认为这是启动崩溃不会记录为一次正常重启次数。max_restarts是连续崩溃多少次后停止重启。这两个参数配合起来可以有效阻止异常代码导致的无脑重启。exp_backoff_restart_delay是启停退避策略值越大每次重启之间的间隔会呈指数增长。对付偶发崩溃导致的频繁重启这个配置比固定restart_delay更保险。watch与ignore_watch控制文件监听功能。本地开发时可以把watch设为true代码有改动就自动重启。但线上环境强烈建议关闭因为监听文件变化会消耗额外的 CPU 和内存而且一旦配置文件被误改可能会触发非预期的重启。如果确实需要在某些场景下用 watch也一定要用ignore_watch排除掉node_modules和日志目录。4.3 环境变量管理的正确打开方式在ecosystem.config.js里env是默认环境变量env_production是在启动时指定--env production才会加载的。更细的玩法是定义多个环境env_test: { NODE_ENV: test, API_URL: https://test-api.example.com }, env_staging: { NODE_ENV: staging, API_URL: https://staging-api.example.com }启动测试环境pm2 start ecosystem.config.js --env test启动生产环境pm2 start ecosystem.config.js --env production很多新手会踩一个坑配置了env_production但启动时没有带--env production结果应用读到的NODE_ENV一直是development进而加载了开发环境的依赖和日志策略。这个错误在线上环境比较隐蔽因为服务看起来是正常运行的只是行为不太对比如数据库用的还是测试库。建议部署脚本里显式加上--env production不要省这一步。4.4 启动配置文件时的模式区分用配置文件启动时也有两种模式分别是pm2 start ecosystem.config.js pm2 start ecosystem.config.js --only my-api第一种会启动配置里apps数组定义的全部应用第二种只启动指定名称的应用。当配置文件里挂了多个服务比如同时有 API 服务和定时任务服务时--only模式非常实用可以单独重启某一个服务而不影响其他进程。实际工作中我还常用到pm2 startOrRestart ecosystem.config.js和pm2 startOrReload ecosystem.config.js。这两个命令的精髓在于如果进程已经存在则执行 restart/reload如果不存在则执行 start。这样部署脚本只需要写一条命令不用先判断进程有没有在跑非常省事。5. 进阶实战集群模式、开机自启与部署优化5.1 Cluster 模式下的端口分配机制Cluster 模式是 Node.js 内置的能力PM2 只是把它封装成了方便配置的形式。核心原理是主进程master负责接收所有网络请求然后通过内部 IPC 通道将请求分发给多个 worker 进程。对使用者而言所有 worker 监听的是同一个端口不需要额外配置负载均衡器。在 PM2 里启用集群模式只需要两步exec_mode: cluster, instances: 2但这里有个细节容易被忽略如果你的应用代码内部使用了有状态的内存存储比如用全局变量保存用户登录态、用内存做缓存cluster 模式可能会带来数据不一致的问题。因为每个 worker 进程都有自己的内存空间用户第一次请求落在 worker A登录态保存在 A 的内存里第二次请求被分发到 worker BB 的内存里没有这份登录态于是用户被判定为未登录。所以使用 cluster 模式前务必确认你的应用是无状态的或者把状态外移到 Redis 等共享存储中。如果你的应用代码里大量使用了global变量且不好改造暂时用 fork 模式多开几个实例配合 Nginx 做负载均衡也是可行的方案。5.2 开机自启与保存进程快照服务器重启之后PM2 本身不会自动恢复之前管理的进程需要借助pm2 startup和pm2 save这对组合拳。第一次运行时直接执行pm2 startupPM2 会检测你的系统类型systemd、upstart 等然后生成一条类似下面这样的命令让你执行sudo env PATH$PATH:/usr/bin pm2 startup systemd -u root --hp /root把输出的命令复制粘贴执行即可。这一步的作用是创建一个系统服务在 systemd 环境下是pm2-root.service让系统在开机时自动拉起 PM2 守护进程。然后执行pm2 save这个命令会把当前pm2 list里保存的进程快照存到磁盘。下次开机时PM2 守护进程启动后会自动读取快照并恢复所有进程。这里有个经验建议每次增删进程后都要重新执行pm2 save。我遇到过几次服务器重启后部分服务没有自动恢复原因就是新增了几个进程但忘了 save。另外如果你改了ecosystem.config.js里的配置并且删除了旧进程重新启动记得再 save 一次确保快照里是最新的配置。5.3 利用 deploy 功能实现零停机部署PM2 内置了pm2 deploy功能可以在远程服务器上拉取代码、安装依赖、割接新版本。这套功能基于 Git 和 SSH适用场景是小团队没有独立 CI/CD 平台时用最简单的配置实现自动化部署。先在项目根目录初始化 deploy 配置pm2 init然后编辑ecosystem.config.js在配置对象外层添加一个deploy字段module.exports { apps: appsConfig, deploy: { production: { user: root, host: [your-server-ip], ref: origin/main, repo: gitgithub.com:yourname/your-repo.git, path: /var/www/my-api, pre-deploy: git fetch --all, post-deploy: npm install --production pm2 reload ecosystem.config.js --env production, pre-setup: } } };命令如下pm2 deploy production setup pm2 deploy production第一次执行setup会在服务器上创建目录结构并克隆代码之后每次发版只需要pm2 deploy production。post-deploy里的pm2 reload是关键步骤它保证新版本代码是通过热更新方式生效的不会出现服务中断。需要提醒几个坑仓库地址建议直接用 SSH 形式用 HTTPS 的话每次都要处理凭证问题服务器的目录权限要提前规划好我之前犯过错误用 root 用户执行部署结果项目文件所有方变成 root导致其他用户操作时出现权限拒绝。最稳妥的做法是创建一个专门的部署账号并赋予项目目录对应的权限。5.4 日志轮转与磁盘清理默认情况下PM2 的日志文件不会自动清理日积月累可能把磁盘撑爆。社区最常用的方案是pm2-logrotate插件。安装pm2 install pm2-logrotate默认配置会自动启用每日轮转保留 30 天内的日志。相关参数可以通过以下命令查看和调整pm2 set pm2-logrotate:max_size 50M pm2 set pm2-logrotate:retain 7 pm2 set pm2-logrotate:compress true第一条表示单个日志文件超过 50MB 就触发轮转第二条表示只保留最近 7 个日志文件第三条表示轮转后的旧日志文件压缩存储节省空间。有一点要注意pm2 install pm2-logrotate会以 PM2 模块的形式常驻运行它本身也会占据很小的内存。在低配服务器上如果不想额外装插件至少要在ecosystem.config.js里把日志路径指向独立目录并配合 crontab 做定时清理。6. 常见问题与排查技巧实录6.1 进程一直处于 restarting 状态怎么办这是新手最常遇到的情况。pm2 list里显示进程状态一直是restarting等几秒又变回online过一会儿又restarting循环往复。排查思路分几步走第一步看错误日志定位崩溃原因。执行pm2 logs my-api --err --lines 200如果没有错误信息把 stdout 日志也看一遍。第二步判断是代码问题还是资源问题。用pm2 monit同时观察 CPU、内存和进程重启次数。如果每个进程内存涨到某个值就重启说明是max_memory_restart阈值设置不当或者代码本身有内存泄漏如果重启间隔很不规律更大可能是代码异常退出。第三步检查是否触发了max_restarts的限制。当进程连续崩溃次数超过max_restartsPM2 会直接放弃重启状态变为errored。这时pm2 logs里会给出错误信息重点看最后几行。6.2 开机自启后服务没有恢复排查顺序如下先运行systemctl status pm2-root以 systemd 为例确认 PM2 守护进程本身有没有起来。如果状态是active (running)再执行pm2 list看进程列表是不是空的。列表为空说明开机时没有执行pm2 resurrect或守护进程读取快照失败。重新执行一遍pm2 save和pm2 startup组合然后手动重启服务器测试。很多情况下问题出在 Node 路径变化比如升级了 NVM 默认版本导致 PM2 的启动脚本找不到 node。这时重新执行pm2 startup会刷新路径。还有一种情况是服务器上有多个用户各跑一套 PM2。pm2 startup生成的服务绑定的是执行命令时的用户。如果你用 root 安装了 PM2 但日常操作用的是www用户开机时系统拉起的是 root 的 PM2里面没有www用户启动的进程。多用户环境下一定要谨慎规划 PM2 的权限归属。6.3 端口被占用导致启动失败场景服务崩溃后端口还没完全释放PM2 立刻拉起新进程结果新进程绑定端口失败。解决办法是在应用的启动代码里设置SO_REUSEADDR大多数 Web 框架默认已经开启同时把restart_delay调大一点。还有另一种端口问题多个 PM2 进程同时监听同一个端口。排查方法lsof -i :3000看到 PID 之后对照pm2 list里的 PID确认是不是有重复进程。如果是 fork 模式下误开了多个实例记得把instances改回1。6.4 环境变量没生效代码里process.env.NODE_ENV始终是undefined或development在线上环境跑了很久才发现。这个问题的原因基本都是启动时没带--env production。你可以这样验证pm2 env 0这个命令会输出指定进程的环境变量列表检查NODE_ENV字段是否符合预期。注意pm2 env后面的数字是进程的 id可以先用pm2 list查看。如果确认环境变量配置无误但还是不生效看看是不是在ecosystem.config.js里同时设置了env和env_production而环境变量名在两个区块里重复定义了。PM2 在处理时会合并配置后定义的字段覆盖先定义的字段。这个时候用一个小技巧在代码里临时打一条日志把process.env完整打印出来再对应 PM2 环境变量列表就能精确定位覆盖关系。6.5 配置文件参数不生效有朋友反映改了ecosystem.config.js里的max_memory_restart重启后pm2 describe显示的还是旧值。这种情况多半是因为没有用配置文件启动进程。如果你之前是通过pm2 start app.js启动的现在改了配置文件再执行pm2 start ecosystem.config.jsPM2 会检测到同名进程存在有可能直接沿用旧参数。正确处理方式pm2 delete my-api pm2 start ecosystem.config.js --env production pm2 save先删除旧进程再用配置文件重新启动确保所有参数都重新加载。另外pm2 restart只会重启进程不会重新加载ecosystem.config.js里的配置变更如果用pm2 reload同样是基于旧配置进行热更新。配置文件改动后想要生效最稳妥的方式就是先pm2 delete再start或者使用pm2 startOrReload ecosystem.config.js。7. 项目扩展从单机到多机部署的衔接PM2 的定位始终是单机进程管理器它不解决跨机器的服务发现和负载均衡问题。当你的流量增长到需要多台服务器共同承载时PM2 的集群模式已经不能满足需求这时候就要把 PM2 和 Nginx/负载均衡器组合起来使用。常见的顶层架构是Nginx 作为反向代理向上对接多个 PM2 管理的 Node.js 实例。Nginx 负责 SSL 终结、静态资源缓存、请求分发PM2 专注于进程的存活和重启。这个架构下PM2 的ecosystem.config.js里instances可以设置为所在机器的 CPU 核心数Nginx 侧的upstream配置对应机器的 IP 和端口。如果项目规模继续变大引入容器编排工具Kubernetes 或 Docker Swarm之后PM2 的角色会进一步弱化因为容器本身有独立的健康检查和重启策略。但即便如此仍有不少团队在镜像内保留 PM2用来管理容器内的多进程和日志收集。这算是一种灵活的组合方式没有绝对的标准答案。这里更想强调的是不要过早引入复杂的架构先把 PM2 的配置和部署摸透。很多中小团队的问题根本不在架构选型而是单机的进程管理都还停留在手工时代。PM2 这套工具用好了足以支撑大多数创业项目的早期阶段等流量真的大了再逐步演进到容器化的方案也不迟。8. 几点个人经验总结最后聊几个我长期实践中养成的习惯不一定适合所有场景但可以参考。第一个习惯是所有 Node.js 服务一律用ecosystem.config.js启动不用裸命令。即便只有一个简单的应用也先把配置文件写好放到仓库里这样任何人都能通过pm2 start ecosystem.config.js --env production快速拉起服务不用去翻历史命令。第二个习惯是每次部署后立即执行pm2 save。这个习惯救过我很多次尤其是服务器意外重启之后能省下大量手工恢复的精力。养成这个习惯之后我基本上不需要担心“忘记保存快照”这类问题。第三个习惯是定期检查日志文件的大小。哪怕装了pm2-logrotate我每两周也会手动看一眼日志目录的磁盘占用。云服务器磁盘告警不是小事尤其在日志量大的业务中一天的日志可能就超过几百 MB。第四个习惯是关于max_memory_restart的。宁可阈值设低一点让进程重启也不要阈值设太高导致内存持续膨胀。有些线上问题之所以“看起来没事”就是因为内存一点点涨上去直到把整台机器拖垮。设置合理的重启阈值让异常进程及时退场整体稳定性会高很多。PM2 的文档和社区资料已经相当丰富了但这篇更偏向于把关键点串起来希望能在你实际运维的过程中节省一些查资料的功夫。如果后续你在使用中遇到别的坑欢迎继续交流。
返回列表