
1. 从跑不挂的进程说起PM2解决了什么问题做后端开发的人十有八九都经历过这样的场景在服务器上敲了一条node app.js接口正常跑起来了日志也在刷刷往外打。你长舒一口气准备去喝杯咖啡结果终端窗口一关服务立刻断了前端那边瞬间报一片“502 Bad Gateway”。又或者服务撑过了关窗这一关却在半夜流量稍高的时候因为内存泄露悄悄挂掉直到第二天用户投诉你才知道。这就是单机进程管理的痛点进程的生命周期和终端绑定没人看管没人守护。PM2这个工具解决的就是这一整套问题。它是目前Node.js生态里最常见的生产级进程管理器内置了负载均衡、自动重启、日志管理、性能监控、开机自启等一系列能力。你可以把它理解成一个尽职尽责的“小区保安”——你的应用是楼里的住户PM2负责看着每户的门谁家异常了立刻处理谁家占用了太多公共资源立刻记录上报。顺便说一句PM2在环境领域也常作为“PM2.5颗粒物浓度”的缩写使用但本文讨论的是软件开发场景下的进程守护工具这两者不是一个东西别搞混了。这篇文章适合谁已经能跑通一个简单的Node服务、但还在用node xxx.js裸奔方式部署的开发者打算把一个脚本项目正式部署到服务器上做长期运营的人以及在多进程管理和线上运维上还比较陌生、想系统补上这一课的朋友。读完这篇文章你会知道PM2到底怎么装、怎么用、配置文件怎么写、常见坑怎么绕并且拿到一套可以直接抄走的线上部署方案。2. PM2的核心设计与安装准备2.1 为什么进程守护不是“多此一举”在理解PM2的具体用法之前有必要先搞清楚它到底干了什么。Node.js本身是单进程模型你跑一个node server.js它就是一个孤零零的进程挂在系统里。这个进程一旦异常退出没有任何机制会把它拉起来就算你想手动拉起来也得先登录服务器、找到项目目录、重新敲一遍启动命令然后眼睁睁看着它在新终端里输出日志再也不敢关窗口。PM2的出现改变了这个局面。它本身以一个守护进程daemon的身份在系统后台常驻管理和监控所有由它拉起的应用进程。当你在PM2里注册了一个应用它就会接管这个应用的整个生命周期进程崩溃了它按照你设定的规则自动重启重启次数超过阈值它把应用标记为异常状态errored并通过邮件、webhook等方式通知你机器重启了它可以通过开机自启机制把应用拉起来一台机器有多个CPU核心它可以用cluster模式启动多个实例把请求分散到不同进程上提高吞吐量。这套逻辑在运维视角里叫“进程守护”本质上和你在服务器上写的各种supervisor脚本是同一回事。但PM2把整个过程做成了标准化的命令行体验并且自带了一套很完整的状态监控面板用起来省心得多。2.2 全局安装与环境检查PM2是一个npm包安装它之前先确认Node.js环境是否就绪。一般来说Node 10.x以上的版本跑PM2的常用功能都没有问题但建议大家至少使用Node 14 LTS或更高版本毕竟新版本的V8引擎在内存管理和性能上有实打实的优化。# 检查Node和npm版本 node -v npm -v然后全局安装PM2npm install -g pm2安装完成后验证一下版本号pm2 -v这里有个小细节如果npm的全局安装路径没有被正确配置到系统环境变量里运行pm2命令时会提示command not found。解决方法是在shell配置文件中把npm全局bin目录加进PATH常见路径是~/npm-global/bin或直接用npm prefix -g查看实际路径。Linux服务器上还有一种方式是使用pm2 startup命令来配置开机自启这会在系统服务层注册一个PM2的启动项后面我会在专门的章节细讲。2.3 几个必须理解的基础概念在正式上手之前我建议先把PM2里的几个核心概念理清楚后面实操才不会一脸懵。应用AppPM2管理的单位对应一个可以被启动的进程。它既可以是一个Node.js服务也可以是Python脚本、Ruby进程、可执行二进制文件等任何你能用命令行启动的程序。进程IDPM2 IDPM2为每个管理中的应用分配一个自增的数字ID便于操作时快速定位。第一次用pm2 start app.js时这个应用在PM2里的ID就是0第二个是1以此类推。进程名称Name你可以给应用起一个可读的名字比如api-server、worker这样在生产环境里看PM2列表时一眼就能知道每个进程是干什么的。不指定名字时PM2会以启动文件的名字作为默认名称。进程模式exec_modePM2支持两种运行模式。fork模式就是普通单进程运行适合大多数场景cluster模式会根据CPU核心数启动多个实例每个实例共享同一个端口由PM2内置的负载均衡器分发请求。cluster模式只对支持多进程的Node.js应用有效不是所有语言都能用。重启次数restart_timePM2会记录每个应用的重启次数。如果应用频繁重启比如启动后就立即崩溃PM2会连续尝试拉起这个数字会快速上涨。这是排查代码缺陷时一个非常重要的信号。运行状态status每个进程在PM2中的状态常见的有online运行中、stopped已停止、errored异常、starting启动中。你会在pm2 list的输出里频繁看到它。这些概念就像汽车的仪表盘你不需要记住每个零件的名字但车上的灯亮了你得知道它代表什么。PM2提供的基础操作就是你的方向盘和油门下面逐一演示。3. PM2的安装与基础使用从零跑起第一个进程3.1 用PM2启动一个简单的Node服务我们先准备一个极简的Node.js服务器用来做演示。// app.js const http require(http); const server http.createServer((req, res) { res.writeHead(200, { Content-Type: text/plain; charsetutf-8 }); res.end(Hello from PM2 demo); }); const port process.env.PORT || 3000; server.listen(port, () { console.log(Server listening on port ${port}); });然后启动它pm2 start app.js执行完这条命令后PM2会在后台拉起这个进程并打印出一张进程列表表格。表格里有进程ID、名称、模式、状态、重启次数、运行时长、内存占用等信息。此刻你应该能看到app.js这一行的状态是online。请特别注意一个体验上的飞跃启动完成后终端是可以正常关闭的。你再也不需要为了让服务活着而挂着一个终端窗口PM2已经把这个进程“托管”了。验证一下在另一个终端里执行curl http://localhost:3000能看到返回的Hello字符串就说明一切正常。3.2 给进程取名并指定启动参数如果服务端口是3000但你有多个PM2应用名字只显示app.js就分不清谁是谁了。所以更正式的做法是pm2 start app.js --name my-api这样PM2进程列表里显示的就是my-api这个名字。如果应用需要环境变量可以用--env或直接在启动命令后追加环境变量的键值对。举例来说假如你的服务需要读取NODE_ENVproduction来判断当前环境可以这样启动pm2 start app.js --name my-api --env production或者更直接的方式NODE_ENVproduction pm2 start app.js --name my-api这两种方式的效果基本一致区别在于--env参数必须配合配置文件里的环境变量组使用而直接写在命令前面则是shell层面的常规做法。后面讲到 ecosystem.config.js 的时候我们会看到更规范的配置管理方式。3.3 查看进程列表与状态信息启动完进程你需要知道当前PM2管理了哪些应用每个应用的状态如何。最常用的命令是pm2 list这个命令会输出一个表格包括字段含义idPM2内部的应用IDname应用名称modefork或clusterstatusonline / stopped / errored↺重启次数cpu当前CPU占用率memory当前内存占用如果你觉得表格里的信息不够直观可以进入实时监控面板pm2 monit这个面板像一个简版的任务管理器按上下箭头可以在不同进程之间切换左侧显示进程基本信息右侧实时刷新CPU和内存曲线。我在排查内存泄漏问题的时候经常开着这个面板观察内存是不是在不断上涨。还有一个命令查更详细的进程信息pm2 describe my-api这个输出包含进程的启动路径、执行参数、日志文件位置、运行环境等所有元数据。如果你需要确认某个进程到底是从哪个目录启动的或者它当前用了哪些环境变量看这个就对了。3.4 停止、重启、删除一个都不含糊日常运维离不开这几个操作命令很直观# 停止指定应用进程仍保留在PM2列表中状态变为stopped pm2 stop my-api # 重启指定应用停止后立即重新启动适用于代码已经更新但不想手动stop再start的场景 pm2 restart my-api # 删除指定应用从PM2列表中彻底移除 pm2 delete my-api这里有一个实际应用中的经验分享如果代码文件没有变化只是想让进程“干净地”重新跑一遍用pm2 restart就够了如果你把代码文件替换成了新版本强烈建议使用pm2 reloadcluster模式下或者先pm2 delete再pm2 start。原因在于restart命令只是把旧进程杀掉再拉起它没有重新读取文件内容吗其实不是restart会重新加载文件但它不重新解析配置文件里的一些启动参数。举一个我踩过的例子我修改了ecosystem.config.js里的max_memory_restart参数以为执行pm2 restart就能生效结果内存限制还是旧值必须把进程删掉重新启动才生效。所以规范的做法是配置文件有变动时用pm2 start重新启动应用代码文件有变动时用pm2 restart或pm2 reload省时省力。3.5 fork与cluster何时需要使用多实例Node.js默认是单线程的也就是说如果你在一个只有4核CPU的服务器上只跑一个Node进程真实有效的计算资源只有1核。cluster模式就是为了榨干多核CPU的价值而生的。启用cluster模式有两种方式。一种是在全新启动时指定实例数pm2 start app.js -i maxmax表示根据机器CPU核心数自动创建对应数量的实例。如果你的机器是4核PM2就会启动4个进程它们共享同一个端口由PM2内置的负载均衡器进行请求分发。另一种方式是在配置文件中写明实例数。这一节先不展开后面会专门讲配置文件。cluster模式适合什么场景适合那些没有在进程内部额外维护全局可变状态的传统Node.js web服务。要注意如果你的服务使用了pm2之外的单机socket比如某些内部调度器、或者依赖本地文件锁的worker模型多实例可能会带来共享状态的问题。这种情况下老老实实用fork模式。4. 深入理解ecosystem.config.js配置文件4.1 为什么建议用配置文件而不是一堆命令参数随着管理的应用越来越多启动参数越来越复杂环境变量、日志路径、实例数、内存上限全塞在命令行里会极度痛苦。比如这条命令pm2 start app.js --name worker --watch --max-memory-restart 300M -i 2 --log ./logs/worker-out.log --error ./logs/worker-error.log --merge-logs看起来已经很长了对不对一旦你需要给一个应用绑定5个环境变量命令行就直接变成天书了。更重要的是命令行参数难以版本化管理下次换一台新服务器你得靠脑子回忆当初是怎么启动的。配置文件的价值在于把应用的启动参数固化成一个可复现、可提交到代码仓库的文件。换新环境时只需要把文件带过去一条pm2 start ecosystem.config.js就能还原出完全一样的运行状态。4.2 一个常用的配置模板PM2官方默认的文件名是ecosystem.config.js它的基本结构长这样module.exports { apps: [ { name: my-api, script: ./app.js, instances: 2, exec_mode: cluster, watch: false, max_memory_restart: 300M, log_date_format: YYYY-MM-DD HH:mm:ss, error_file: ./logs/err.log, out_file: ./logs/out.log, merge_logs: true, env: { NODE_ENV: production, PORT: 3000 } } ] };逐项解释一下关键字段name应用名称对应命令行里的--name。script入口脚本路径。注意区分场景如果你的入口文件是一个本地文件如app.js写相对路径即可如果是npm全局包如某个CLI工具可能需要写全路径或用interpreter配合指定。instances实例数量。可以写数字也可以写max表示用满所有CPU核心。exec_mode进程运行模式。fork或cluster。watch是否开启文件监听。开启后源文件一旦发生变化PM2会自动重启应用这对本地开发很友好但生产环境慎用。max_memory_restart内存超过这个阈值时自动重启进程单位可以是K、M、G。error_file/out_file标准错误和标准输出的日志文件路径。merge_logs在cluster多实例模式下把所有实例的日志合并写入同一个文件避免一个实例一个文件导致日志分散。env进程看到的环境变量集合。配置文件写好后启动命令变成pm2 start ecosystem.config.js也可以指定启动配置里的某一个应用pm2 start ecosystem.config.js --only my-api4.3 环境变量分组dev与prod各配一套实际开发中本地环境和生产环境的配置往往是不同的本地端口可能是3000数据库指向本地生产环境端口可能是80数据库指向云数据库。PM2的配置支持按环境拆分使用方法是在env的基础上增加带下划线后缀的分组module.exports { apps: [ { name: my-api, script: ./app.js, instances: 1, exec_mode: fork, env: { NODE_ENV: development, PORT: 3000 }, env_production: { NODE_ENV: production, PORT: 8080 } } ] };启动时通过--env指定使用哪一组# 本地开发 pm2 start ecosystem.config.js # 生产环境 pm2 start ecosystem.config.js --env production注意--env production并不是简单地替换掉env的默认值而是在默认可选值的基础上合并了env_production里定义的变量。同名变量会被覆盖不同名的变量会被追加。这个机制在管理多个环境时非常灵活如果你有测试环境、预发布环境照葫芦画瓢加一个env_staging即可。4.4 配置文件里的坑与细节配置文件虽好用但有几个细节值得专门提醒。第一路径尽量用绝对路径或相对于项目根目录的路径。我见过有人写error_file: ./logs/err.log结果PM2把日志文件写到了PM2自己的安装目录下因为它把当前工作目录理解成了别的地方最后日志“离奇消失”。稳妥的做法是在配置里用__dirname拼接路径const path require(path); module.exports { apps: [ { name: my-api, script: ./app.js, out_file: path.join(__dirname, logs, out.log), error_file: path.join(__dirname, logs, err.log) } ] };第二配置文件里别把密码和密钥以明文写死。配置文件通常会被提交到git仓库密钥一旦泄露就是安全事故。生产环境的敏感变量应该使用环境变量注入或者在部署流程中用CI/CD的变量机制动态生成配置。第三watch模式在生产环境要谨慎开启。如果代码目录里同时存放了日志文件而日志文件又恰好被watch模块扫描到并触发了重启你会看到一个“文件变化引起重启重启后生成新日志新日志再次触发重启”的无限循环。真的要在生产环境用watch一定要配置ignore_watch把日志目录、临时目录、node_modules等排除掉。5. 日常操作全掌握日志、监控、资源限制与开机自启5.1 实时查看和追溯日志日志是线上排查问题的生命线。没有日志出了问题只能靠猜。PM2把应用的stdout标准输出和stderr标准错误分别写入日志文件同时提供了几个方便的查看命令。实时跟踪日志也就是“把日志像流水一样打印到当前终端窗口”pm2 logs my-api如果只想看某个进程的最后50行日志可以加--lines参数pm2 logs my-api --lines 50清空所有日志文件pm2 flush这里必须强调一点PM2本身不是日志的最终归档系统。它的日志默认存在用户目录下例如~/.pm2/logs/时间久了文件会越滚越大甚至在服务器磁盘空间不足时拖垮整台机器。生产环境建议做两件事一是给应用配置日志轮转logrotate二是结合ELK或Loki这类集中日志平台把日志采集走。PM2有一个官方的日志切割插件叫pm2-logrotate安装方式很简单pm2 install pm2-logrotate插件安装后默认每天切割一次日志并保留最近10个文件。也可以自己调整参数pm2 set pm2-logrotate:max_size 10M pm2 set pm2-logrotate:retain 7 pm2 set pm2-logrotate:compress true这样日志文件超过10MB就轮转、保留最近7个、轮转时压缩为gz格式磁盘空间的压力会小很多。5.2 资源使用监控CPU与内存的可视化前面提到过pm2 monit可以实时看CPU和内存的曲线。如果你需要把它集成到自己的监控告警体系里PM2还提供了HTTP API接口默认端口为9615。这个接口可以对外输出所有进程的指标数据curl http://localhost:9615/metrics配合Prometheus的scrape_config配置就能把PM2的指标接入到Grafana面板里做可视化监控。如果你已经在用Prometheus生态这是一个成本极低的监控接入方案。如果还没搭建监控体系只想在出问题时快速看一眼pm2 monit完全够用。5.3 限制进程内存防止内存泄露拖垮服务器Node.js进程最常见的一个问题就是内存泄露某个全局变量不断累积、某个闭包没有释放、或者某个第三方库存在隐性问题进程内存占用会随时间线性增长最终导致服务器swap吃紧、响应变慢、甚至被OOM killer杀掉。PM2提供了一个非常实用的参数max_memory_restart。当进程内存占用超过设定值时PM2会自动重启该进程让内存回到初始水平。命令行方式启动时pm2 start app.js --max-memory-restart 500M配置文件方式{ name: my-api, script: ./app.js, max_memory_restart: 500M }这个阈值怎么设我个人的经验是先正常压测一下服务观察稳定状态下内存占用是多少然后在这个基础上乘以1.5到2作为重启阈值。阈值设得太低会导致进程频繁重启接口响应不稳定设得太高则起不到守护作用等到内存炸了才重启为时已晚。5.4 开机自启服务器重启后自动恢复所有服务服务器总会因为各种原因重启——机房维护、内核升级、断电重启。如果你人不在电脑前重启后所有服务都不会自动恢复等于线上服务直接宕机。PM2提供了开机自启的配置命令。第一步执行pm2 startup执行后PM2会打印一段提示大意是让你复制一条以sudo env PATH$PATH...开头的命令去执行。我见过很多人在这里直接漏看提示以为pm2 startup本身已经完成了配置。实际上不同操作系统systemd还是upstart生成的系统服务脚本不同你必须把系统提示的那条命令原样复制执行。第二步把当前PM2里已注册的应用状态保存为快照pm2 save这样PM2就把当前运行的进程列表固化到一个dump文件里。下次开机时系统的自启脚本会启动PM2守护进程然后PM2按照dump文件自动恢复所有应用。有一点要注意如果你后来新增或删除了应用记得重新执行pm2 save否则开机恢复的仍然是旧快照。这个操作建议写进自己的部署脚本或团队的手册里。5.5 优雅关闭让你的应用体面离场进程被重启或停止时如果你的应用正在处理一些重要任务比如同步数据库、写文件、上报数据直接强杀可能会导致数据不完整。PM2支持在关闭进程时给应用发送指定的信号默认是SIGINT相当于CtrlC。你的应用只需要在代码里监听这个信号process.on(SIGINT, () { console.log(Received SIGINT, cleaning up...); // 这里做清理工作比如关闭数据库连接 process.exit(0); });如果调整信号类型可以在启动命令或配置文件中指定pm2 start app.js --kill-timeout 5000kill_timeout表示PM2发出关闭信号后等待多少毫秒再强制杀死进程。我给自己的服务设置的是5000毫秒给足应用5秒时间处理残余任务。6. 常见问题速查与几条实战建议6.1 进程频繁重启状态始终是errored遇到这种情况第一件事是查看错误日志pm2 logs my-api --err大部分应用启动即崩溃的原因非常直白端口被占用、环境变量缺失、Node版本不对、入口路径写错、依赖没有安装完整。逐项排查就能定位。如果错误日志里没有内容再确认一下PM2的工作目录是否和预期一致pm2 describe my-api看exec cwd这一行它就是进程启动时所在的工作目录。如果这个路径和你项目的实际路径不一致应用可能根本找不到它依赖的配置文件。6.2 cluster模式下某个实例反复重启cluster模式多实例情况下偶尔会见到某个实例反复重启其他实例正常。这种情况多半是实例之间共享了某些不可共享的状态比如同时写同一个文件、同时在内存里维护了一份冲突的缓存、或者数据库连接数被打满。排查思路是先降为单实例试试是否还复现如果单实例没问题再从共享资源的并发写入入手排查。6.3 日志文件突然不写了先检查是不是磁盘满了df -h再检查日志文件路径有没有被误改。如果是pm2 flush之后日志不写了大概率是日志文件句柄的问题重启一下应用即可恢复。6.4 开机自启后应用没有恢复杀掉当前进程、执行一遍pm2 save再重启验证。如果问题依旧检查pm2 startup生成的系统服务是否处于启用状态systemctl status pm2-用户名如果服务状态是inactive手动启动它然后再次执行pm2 save。6.5 一个部署脚本模板最后分享一个多环境通用的部署脚本思路。新服务器上要跑一套完整的部署按照这个顺序执行基本不会漏# 1. 安装Node和PM2具体版本自行调整 curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash - sudo apt-get install -y nodejs sudo npm install -g pm2 # 2. 拉取代码并安装依赖 cd /var/www/my-api git pull origin main npm install --production # 3. 启动应用配置文件名按实际项目调整 pm2 start ecosystem.config.js --env production # 4. 保存进程快照并配置开机自启 pm2 save pm2 startup这套流程是通用的骨架有几个地方可以替换成你自己的项目参数目录路径、仓库地址、依赖安装命令等。6.6 几个经验之谈文章写到这里最后再分享几点我在真实生产环境中体会最深的东西。第一PM2不是银弹。它能守护进程、能自动重启、能监控资源但它无法修复有bug的应用代码。如果进程每天都在反复重启你的首要任务是找到崩溃根因而不是简单地把重启阈值调大掩盖问题。第二不要在多个项目之间共享同一个PM2守护进程除非你清楚自己在做什么。比如CI/CD工具、定时任务、Web服务混在同一个PM2里一次误操作删错了进程连带其他项目一起停摆这种事故我见过不止一次。建议不同的独立服务用不同的PM2实例或者用PM2_HOME环境变量隔离各自的配置目录。第三用PM2管理脚本类任务如定时爬虫、一次性批处理时注意进程退出后的处理方式。一个脚本跑完正常退出PM2默认会判定为失败并尝试重启它这会导致脚本被反复执行。这种情况可以在配置里设置autorestart: false让进程正常退出后保持退出状态不再拉起。第四版本更新是一种经常被忽略的坑。Node.js升级比如从14升到16后老版本下安装的PM2可能存在兼容性问题新版本语法上用了新的V8 API可能会出现一些诡异的行为。升级Node后记得重新安装一次全局PM2并重启所有应用。7. 结尾不整总结只聊一点观察说句实话我最初接触PM2的时候只把它当做一个“不用关终端就能跑服务”的工具后来用着用着才意识到它真正的价值在于把这些原本要写一堆shell脚本才能做到的“进程兜底能力”做成了开箱即用的标准体验。尤其是开机自启和cluster模式这两个功能在裸奔的部署方案里实现起来不是一般的麻烦。如果你现在还在用终端挂着Node服务跑生产我建议你把PM2装上哪怕只是体验一下“启动后再也不用抬头看终端窗口”的那种踏实感。如果你已经在用PM2不妨对照这篇文章检查一下自己的日志轮转和开机自启配置这两个环节是最容易被人遗忘却影响最大的。说到底工具终究是为人服务的掌握得越扎实踩的坑就越少。希望这篇分享能让你在生产部署的路上少走一点弯路。