1. 为什么Node服务都需要一个PM2
1.1 新手最常见的三大崩溃现场
我先说三个场景,如果你已经遇到过了,那这篇手册你大概率是需要的。
第一个场景,你在本地把接口调通了,node app.js跑得挺好,兴冲冲部署到服务器上。结果SSH窗口一关,服务就没了。为什么?因为进程是挂在你的终端会话下面的,终端退出,会话挂断,子进程被系统回收。这是新手第一次意识到“原来这样跑不行”。
第二个场景,服务半夜崩了。你在睡梦中,用户已经骂了一轮又一轮,第二天早上才发现进程不在了。你翻开日志,发现是某个未捕获的异常把进程直接带崩,但关键是:你不知道它什么时候崩的,也不知道怎么让它自己活过来。
第三个场景,一个Node实例单线程跑着,CPU只用一个核,前端一旦有稍微高一点的并发请求,响应时间肉眼可见地涨。你想多开几个实例,但手动node app.js开八个窗口?这不现实,而且端口也会冲突。
这三个问题,本质上是同一件事:Node进程缺少一套系统化的守护和管理方案。PM2解决的就是这个问题。它不仅能在进程退出后立刻拉起新进程,还能收集日志、做负载均衡、设置内存上限、配置开机自启。对于刚接触服务器部署的新手来说,PM2几乎是最短路径上必经的一个工具。
1.2 PM2解决的不只是“重启”这一件事
很多人对PM2的理解停留在“进程守护”四个字上,觉得它就是一个自动重启的工具。这是低估了它。PM2的核心是一个完整的进程生命周期管理方案,守护只是其中最基础的一项。
我把PM2的核心能力拆成四层:
第一层,进程托管。pm2 start app.js之后,进程进入PM2的监管列表。退出自动拉起,崩溃自动重启,这个逻辑是默认开启的,不需要你做额外配置。
第二层,日志管理。PM2会把进程的stdout(标准输出)和stderr(标准错误输出)分别收进日志文件,并带有时间戳。你不需要自己写fs.appendFile去记日志,也不需要借助forever那种原始方案,一条pm2 logs命令就能实时看到输出。
第三层,负载均衡。PM2内置的cluster模式可以让你一行命令把服务以多实例方式跑起来,利用服务器多核CPU的能力,并在实例之间分发请求。当年很多Node服务扛不住压力,其实不是代码问题,而是根本没有用上多核能力。
第四层,运维自动化。包括开机自启、优雅重启、内存监控、CPU监控、部署流程配合。这些都是生产环境必须的东西,也是新手最容易忽略的。
这篇手册我按“先会用,再理解,最后排查”的路线来写。所有命令我都按实际场景来演示,你跟着敲就行。
2. 安装与第一条启动命令
2.1 安装前的环境准备
PM2本身是一个npm全局包,安装它之前你需要确保Node环境已经就绪。Node的版本建议8以上,实际上我见过有人在Node 6上跑PM2也能用,但新版PM2特性(比如某些监控指标)还是需要新版本支持。我自己的习惯是保持Node LTS版本,避免不必要的兼容性问题。
安装命令很简单:
npm install -g pm2装完之后验证一下:
pm2 --version如果能看到版本号,说明安装成功。这里有一个小坑:有些服务器的npm全局bin目录不在PATH里,安装完会提示command not found。解决方式是查看npm prefix -g输出的目录,把它加到PATH里。这个情况在服务器上比本地开发环境更常见。
如果你用的是yarn或者pnpm,对应命令是:
yarn global add pm2 pnpm add -g pm2安装完成后,还可以给PM2开启启动时的自动补全,这个看个人习惯,我是一直用着:
pm2 completion install然后重开一个终端才会生效。这个补全对新手其实挺有用的,能帮你少打不少命令。
2.2 第一条启动命令与通用命令速查
准备工作做完,我们进入正题。假设你的项目入口文件是app.js,目录是/var/www/myapp,启动命令就一句话:
cd /var/www/myapp pm2 start app.js --name myapp--name是给进程起一个别名,方便后续管理。如果不指定,PM2会用文件名作为默认名称。
启动之后你会看到一张表格,里面有几个关键信息:
id:进程ID,PM2内部管理的编号,从0开始。name:进程别名。mode:fork或cluster,目前是fork模式。status:online就是正常运行,stopped是已停止,errored是异常退出。restarts:重启次数。这个很关键,后面排查崩溃问题就看它。
启动只是第一步,接下来是几个高频命令,你最好背下来:
pm2 list # 查看所有进程列表 pm2 logs myapp # 实时查看日志 pm2 restart myapp # 重启进程 pm2 reload myapp # 优雅重载进程 pm2 stop myapp # 停止进程(进程保留在列表里) pm2 delete myapp # 删除进程(从列表里移除) pm2 monit # 进入实时监控面板这里区分一下restart和reload。restart是直接杀掉进程再启动,会有一瞬间的断连;reload是针对cluster模式下的0秒停机重启,它会逐个替换实例,滚动式完成。如果是fork模式,reload和restart的表现基本一致,没有太大区别。
还有一个细节:pm2 start app.js这一步,PM2默认会把进程名称设置为app。如果你同时管理多个项目,每个项目入口恰好都叫app.js,就很容易搞混。所以从一开始就养成加--name的习惯,后面省心很多。
3. 看懂PM2的进程状态
3.1 status字段代表什么
新手最容易困惑的,就是pm2 list里那几个英文单词。我一个个说。
online:进程活着,正常接受请求。这个没什么好说的。
stopped:进程被手动停止了。执行pm2 stop myapp就是这个状态。注意,此时进程还在PM2的列表里,你可以随时pm2 start myapp再次拉起它。如果你执行pm2 delete myapp,那就是彻底移除,进程没了,列表里也没了。
errored:进程异常退出了。这个状态要重视,说明进程跑了但没跑起来,可能是有未捕获的异常,也可能是端口被占用。大多数时候,PM2会在它退出后尝试重启,你会发现restarts数字一直在涨。如果涨得很快,说明有严重问题,比如端口被占或者配置加载失败。
还有一个状态叫stopping,一般是正在停止的过渡状态,不用太在意。
每次启动或重启后,建议看一眼这几点:status是不是online,restarts是不是在增长,uptime是不是已经持续了一段时间。如果restarts从0变成几十,那基本可以判定存在反复崩溃的循环。这时候别在pm2 restart上纠结,要想办法看日志找根因。
3.2 日志查看与日志切割
我把日志单独拉出来讲,因为它对排查问题太重要了。
PM2默认会把进程的stdout写进~/.pm2/logs/myapp-out.log,stderr写进~/.pm2/logs/myapp-error.log。如果默认路径找不到,可以用pm2 info myapp查看详情,里面会列出两个日志文件的完整路径。
实时看日志的命令:
pm2 logs myapp这个命令会进入一个实时滚动的视图,类似tail -f的效果,能看到所有的输出。如果想看最近的200行:
pm2 logs myapp --lines 200不加--lines的话,默认会给最近20行。这个参数在生产环境排查问题时很好用,日志太长了就指定行数,节省时间。
这里有个坑要提醒:很多新手在自己的代码里用console.log输出内容,发现pm2 logs里看不到。大部分情况是因为进程还没把缓冲区的数据刷出来,特别是console.log输出量很小的时候,缓冲区没满就不会主动写入。处理方法有两个,一是用pm2 flush清空缓冲区,二是启动时加--merge-logs参数。其实更推荐在代码里用成熟的日志库(比如log4js或winston),直接把日志写入文件,这样更可控,PM2这边只需要管理文件大小。
日志文件无限增长是个大问题。默认情况下PM2不会按大小切割日志,时间长了你的磁盘会被占满。解决办法是安装PM2的日志切割模块:
pm2 install pm2-logrotate这个模块可以让日志按天轮转,也可以设置大小上限。常用配置:
pm2 set pm2-logrotate:max_size 50M pm2 set pm2-logrotate:retain 7max_size是单个日志文件超过多大就切割,retain是保留几天的历史文件。我建议一上来就配好这两个参数,免得一个月后发现磁盘被日志塞满了。
4. 生产环境的配置文件
4.1 ecosystem.config.js
每次都用pm2 start app.js来启动,进程少的时候还行,一旦进程多了,你会发现命令行参数长得没法看:要指定--name、要指定NODE_ENV、要指定--max-memory-restart、还要指定--watch。这种情况下,配置文件就是必需品。
PM2支持的配置文件格式是JavaScript,默认文件名ecosystem.config.js。一个简单的配置长这样:
module.exports = { apps: [ { name: "myapp", script: "./app.js", instances: 1, exec_mode: "fork", env: { NODE_ENV: "production", PORT: 3000 }, max_memory_restart: "300M", log_date_format: "YYYY-MM-DD HH:mm:ss", error_file: "/var/log/myapp/error.log", out_file: "/var/log/myapp/out.log" } ] };然后启动命令变成:
pm2 start ecosystem.config.js这个配置文件的好处是明显的:环境变量、日志路径、重启策略写在一起,团队协作时大家看到的是同一份配置,不会因为你少了一个参数导致生产环境和本地环境行为不一致。我见过不少生产事故,根子就是启动命令在文档里和实际操作里对不上。配置文件能从根本上避免这个问题。
文件里的env字段代表默认环境变量。如果你有多套环境,可以这样拆:
module.exports = { apps: [ { name: "myapp", script: "./app.js", env: { NODE_ENV: "development", PORT: 3000 }, env_production: { NODE_ENV: "production", PORT: 8080 } } ] };启动生产环境时加--env production:
pm2 start ecosystem.config.js --env production注意,--env production后面的production对应配置文件里的env_production字段,PM2会把这个对象里的字段合并进环境变量。这个机制是固定的,env字段是基础,env_xxx字段按需覆盖。
4.2 cluster模式与实例数怎么定
这是PM2最值得花时间理解的一个功能。
Node.js是单线程的,默认情况下一个进程只能跑在一个CPU核心上。如果你的服务器是4核的,一个Node进程最多只能吃到其中一核的性能,剩下三核闲着。这在并发量上来之后是明显的瓶颈。
PM2的cluster模式可以让你把一个应用复制成多个进程实例,每个实例绑定到不同核心,由PM2内置的负载均衡器把请求分发到各个实例上。配置方式:
module.exports = { apps: [ { name: "myapp", script: "./app.js", instances: 4, exec_mode: "cluster" } ] };启动后你会在列表里看到4个进程,共享同一个名字和ID段。
instances填几最合适?这个问题没有标准答案,但我可以给你一个经验法则:先看服务器是几核的,先按核数填,然后压测。如果CPU没有跑满,就逐步增加,同时观察内存。
填-1或max的话,PM2会自动使用服务器CPU的核心数作为实例数:
instances: "max"这个写法适合大多数场景,但有一个前提:你的应用必须是无状态的,或者说状态可以外部化。比如session不能存在内存里,要放到Redis;文件上传不能写到本地磁盘,要放到对象存储。否则cluster模式下不同实例之间数据不同步,你会遇到各种“不是必现但很致命”的诡异问题。
还有一点要强调:cluster模式只适用于HTTP服务,因为它依赖PM2内置的端口共享机制。如果你的脚本是定时任务、消息队列消费者这种不监听端口的进程,不要用cluster模式,fork模式一个实例就够了,多开反而会造成任务重复处理。
4.3 内存限制与自动重启
Node进程有个常见问题:内存泄漏。泄漏到一定程度,进程会变得非常卡,甚至被系统杀掉。PM2提供了max_memory_restart参数来主动干预:当进程内存超过设定值,自动重启。
max_memory_restart: "300M"这里怎么取值?我个人的习惯是:先正常跑一段时间观察基准内存,然后在这个基础上加50%作为阈值。比如你的服务稳定运行时占用150M内存,那就设成300M,留出一定的缓冲。如果你设置的阈值刚好卡在正常波动范围内,会导致频繁重启,反而不稳定。
重启策略是:超过阈值后,PM2会发送一个重启信号,进程被重新拉起。但这只是治标,内存泄漏的根因还是在代码里。实际遇到泄漏问题时,先用pm2 monit观察内存曲线,确认是平滑上涨还是阶梯式增长,然后结合日志和代码逐步排查。
5. 开机自启与开机任务
5.1 自启原理
服务器重启之后,PM2管理的进程不会自动恢复。如果你没有配置开机自启,系统重启后你的服务就全挂了,而且你可能要等到用户反馈才知道。对新手来说,这个坑几乎是必踩的。
PM2解决这个问题的方案很聪明:它生成一个系统服务脚本,交给系统的初始化系统(systemd)来管理。这样开机时systemd会启动一个PM2守护进程,然后由守护进程恢复所有之前管理的进程列表。
执行下面的命令:
pm2 save pm2 startuppm2 save是把当前进程列表快照保存到~/.pm2/dump.pm2,这是重启后恢复的依据。pm2 startup会检测当前的初始化系统,输出一条需要执行的命令,你按提示复制执行即可。
如果是systemd环境,涉及的命令大致是:
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u <用户名> --hp <用户主目录>执行完可以验证一下:
systemctl status pm2-<用户名>看到active (running)就说明成功了。如果状态不是active,大概率是用户或路径不对,重新执行pm2 startup,仔细看它提示的那条命令。
5.2 实操配置
完整的配置流程我建议这么做:
第一步,把项目启动好,确保一切正常。第二步,执行pm2 save,保存当前进程快照。第三步,执行pm2 startup,按提示完成systemd服务注册。第四步,手动重启服务器(或者用systemctl reboot),验证进程是否自动恢复。
有一个操作顺序要特别注意:先pm2 save再pm2 startup,还是反过来?严格来说都行,但我的建议是先把服务跑起来、确认没问题,再保存、再配置自启。因为pm2 save保存的是“当前正在跑的进程列表”,如果列表是空的,那你保存的就是一个空快照,重启后什么都不会恢复。
还有一个容易忽略的点:如果你后面新增了进程,必须重新pm2 save,否则新增的进程不会出现在自启列表里。我见过有人配置过一次自启之后,后面加的进程重启后全部消失,就是因为漏了这一步。
6. 常用进阶功能
6.1 负载监控
pm2 monit是我在生产环境用得很频的交互界面。执行后你会看到每个进程的CPU、内存、事件循环延迟等信息,还有日志实时输出。
监控面板里最有价值的是“自定义指标”区域,比如HTTP请求处理量、并发连接数,前提是你在代码里用PM2的API上报:
const pm2 = require('pmx').init();这个属于进阶玩法,新手阶段可以先不用管,但了解一下有好处:PM2不只是被动地看进程状态,它还能主动采集业务层的指标。把这些指标接到监控系统里,你就有了完整的可观测性。
如果你不满足于终端里的monit,PM2还有一个Web界面,通过pm2 plus开启,它是商业化服务,支持远程监控、告警、异常检测。个人项目或小团队不一定要用,但如果你管着几十台机器,这种集中式监控的价值就很明显了。
6.2 零停机部署重载
在cluster模式下,你改了代码要重新加载,请用reload而不是restart:
pm2 reload myappreload的逻辑是逐个重启实例。假设你有4个实例,PM2会先重启第一个,等它起来并且健康检查通过,再重启第二个,直到全部替换完成。这个过程不会出现服务整体不可用的窗口,用户请求始终能被处理。
对比一下restart:它会一次性把所有实例全部停掉再全部启动,中间有个时间窗口是服务不可用的。对于生产环境来说,这个窗口哪怕只有几百毫秒,也可能造成请求失败。
如果你用的是fork模式,reload没有实际意义,它和restart差不多,因为只有一个实例,没法滚动。所以这也是为什么我建议生产环境尽量用cluster模式。
部署更新时,我一般这样做:先构建新版本(如果有构建步骤),然后pm2 reload ecosystem.config.js,等所有实例滚动更新完,再验证接口。整体过程几十秒内完成,用户无感知。
7. 常见问题排查实录
7.1 进程反复重启
现象:pm2 list里restarts数字不断增加,或者status一直在errored和online之间横跳。
第一步,看日志。命令是:
pm2 logs myapp --err --lines 100重点看有没有SyntaxError、Module not found、EADDRINUSE、TypeError这类关键词。大多数反复重启都是这几个原因:代码语法错误、依赖没装全、端口被占、环境变量缺失。
第二步,如果是语法错误,本地先修好再部署。你可能会觉得这是废话,但实际上很多人先启动再看日志,然后又去改代码,改完只重启进程不重新构建,导致代码没变。记得有构建步骤的一定要先构建。
第三步,如果是依赖缺失,npm install重新装一遍,确保node_modules完整。
7.2 端口被占用
启动时报EADDRINUSE,说明端口被某个进程占用了。先用下面的命令找到占用进程:
lsof -i :3000或者:
netstat -tunlp | grep 3000找到PID后,确认这个进程是否还需要。如果不需要就杀掉,然后重启你的应用。如果你确定这个端口应该给PM2管理,检查一下是不是上一个进程还没完全退出,比如pm2 stop之后端口没有立即释放,等几秒再启动。
7.3 环境变量不生效
在ecosystem.config.js里配了env,但代码里读到的却是undefined或者旧值。这个大概率是缓存问题。
PM2会缓存环境变量。改动配置文件后,重新启动时用:
pm2 restart myapp --update-env还要注意:pm2 restart和pm2 start在加载环境变量上的行为不完全一样。restart会保留启动时的环境,start会重新读取配置文件。所以改完配置后,建议用pm2 delete myapp再pm2 start ecosystem.config.js,比restart更干净。
另外,配置文件里的env_production只有你在命令里加了--env production才会加载,不加就永远只有env里的字段。这个坑我踩过不止一次。
7.4 日志找不到
如果你配置了自定义日志路径,比如/var/log/myapp/error.log,但是文件一直没生成,大概率是目录权限问题。PM2以当前用户身份运行,要去写/var/log下的目录,需要那个目录有写权限。
遇到这种情况,我的建议是:把日志目录放在应用目录下,比如/var/www/myapp/logs,然后用error_file和out_file指向它:
error_file: "/var/www/myapp/logs/error.log", out_file: "/var/www/myapp/logs/out.log"改完配置,重启之后看日志文件有没有生成。如果还是没有,用pm2 logs看看有没有报错信息,定位是路径问题还是权限问题。
最后再说一个我自己的经验:PM2虽然好用,但它不是万能的。进程守护、日志收集、负载均衡、开机自启这些都是基础设施层面的保障,真正决定服务质量的还是代码本身。一个死循环的代码,PM2再怎么重启也会被下一个死循环拖垮;一个内存泄漏不修的应用,max_memory_restart只会让它陷入重启掉线又重启的循环。把PM2当成一个可靠的工具,用好它,但不要依赖它来掩盖代码的毛病。先学会用命令,再学会看状态,然后学会配配置文件,最后你就能把这些东西串起来,搭出一套基本让人放心的Node服务部署体系。