ARTICLE DETAIL

资讯详情

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

PM2详解:Node.js进程守护、日志管理与零停机部署实战

PM2详解:Node.js进程守护、日志管理与零停机部署实战 在Node.js应用的部署运维里我几乎没见过哪个项目最后能绕开PM2。很多人最开始只用node app.js把服务跑起来觉得一切正常一旦遇到服务器重启、进程崩溃、日志丢失就得半夜爬起来手动敲命令。PM2这个进程管理器本质上就是给Node服务配了一个管家接管启动、守护、日志、负载均衡这些脏活累活。这篇文章我把PM2从入门到落地经常用到的东西捋一遍包括核心原理、配置文件、常用命令和我在生产环境踩过的真实坑希望能让正准备上PM2或者已经在用但没搞透的人省点弯路。1. 没有进程管家时Node.js部署的尴尬1.1 一个Nodemon跑得好好的却崩了场景很多新手第一次部署Node应用用的是node server.js甚至nodemon server.js跑在服务器上。开发环境里nodemon确实香改代码自动重启但到了生产环境它就变成了一个定时炸弹。原因很简单nodemon是为了开发时监听文件变化设计的它没有守护进程的能力。你把它挂在一个SSH会话里只要你要断掉终端它就可能一起消失更别提进程崩溃以后自动拉起来。我之前遇到一个真实场景一台云服务器上跑了三个Node服务因为内存紧张其中一个进程被系统OOM killer杀掉了。当时几个服务都打印在同一个终端里终端早就关了根本没人发现。等用户反馈接口超时已经是第二天早上我登录上去一看进程没了日志也没了只能靠回忆排查。从那一刻起我就意识到手动管理进程这件事在生产环境里根本不可接受。1.2 进程管理器的核心职责重启、日志、守护进程管理器要解决的核心问题总结起来就是三件事进程死了要自动拉起来日志不能随随便便丢应用要能脱离终端独立运行。PM2恰好把这三件事做成了开箱即用的标配。先说守护。PM2启动应用后应用进程会作为PM2的子进程运行PM2本身是后台daemon进程所以你的Node进程不再依赖SSH会话。即使你退出终端应用还在跑。即使PM2 daemon自身挂掉服务器重启时也可以设置开机自启。再说自动重启。进程崩溃、异常退出、内存超过阈值PM2都会按照规则重启应用还能设置重启次数限制超过次数就进入error状态避免无意义的死循环。日志这块PM2默认会把应用的标准输出和错误输出分别收集到~/.pm2/logs下面并且支持按文件大小自动分割。你不需要再自己写log4js的file transport直接pm2 logs就能实时看所有应用的日志。当然如果是复杂的业务日志还是建议应用内做结构化日志PM2处理的是进程日志和输出日志这一层。另外PM2还能做零停机重载、负载均衡、开机自启、环境变量管理。这些放到后面细说。一句话没有进程管理器你在生产环境就是赤手空拳有了PM2至少有个能干活的帮手。2. PM2的工作原理master进程与worker进程的协作2.1 进程模型deamon与子进程PM2的架构其实很简单它启动后先创建一个daemon进程这个daemon负责管理所有的应用进程。每个应用进程都是daemon的子进程daemon和子进程之间通过IPC通信子进程的状态、日志、退出信号都会实时上报给daemon。你可以用pm2 list查看当前管理的所有应用每一行就是一个应用实例。这里面有个概念要搞清楚PM2里说的进程数instances不一定是一台机器的物理进程数它可以是多个worker进程。比如你在配置里设置instances: 4PM2会启动4个cluster模式的worker进程这些worker共享同一个端口由master进程Node的cluster模块负责负载均衡。PM2还有一层环境隔离的功能。不同的应用可以设置不同的NODE_ENV、环境变量、工作目录和启动路径PM2会在启动子进程时注入这些配置。这样一来一台服务器上跑多个Node版本、多个环境配置的应用也变得很清晰。2.2 为什么需要master进程管理worker进程理解PM2之前先说Node.js自己的cluster模块。Node的cluster模块允许一个master进程fork多个worker进程master负责接收外部请求并通过轮询机制分发给worker。PM2的cluster模式封装了这个能力你不需要在代码里手动写cluster.fork()只需要在PM2配置文件里设置exec_mode: cluster和instances: maxPM2会自动根据CPU核数启动对应的worker数量。这里有很关键的一点如果你用了cluster模式你的应用代码里不能使用那些依赖进程内存共享的有状态方案比如全局变量存session因为每个worker进程的内存是独立的。如果你需要共享状态就要用Redis这类外部存储。PM2的daemon和Node的cluster master不完全一样。PM2 daemon是整个PM2的入口它监控的是应用进程可能是单个进程也可能是cluster worker。当你使用pm2 reload时PM2会逐个重启worker而不是把整个应用一次性干掉这就是零停机重载的基础。2.3 安装PM2和初始化环境PM2是一个npm全局包安装很简单npm install -g pm2装完以后验证一下版本pm2 --version我建议在服务器上安装Node时顺便把npm全局环境配置好避免因为权限问题装不上全局包。如果用nvm管理的Node记得确认npm全局bin目录在PATH里。在项目目录里通常会用PM2的配置文件ecosystem.config.js来定义启动参数而不是每次启动时手动敲一长串命令。这样可以统一管理也方便团队协作。3. 从零开始用PM2托管你的第一个Node应用3.1 基础启动命令pm2 start拿到一个现成的Node项目最快的方式是pm2 start app.js --name my-app这条命令会让PM2以默认的fork模式启动app.js应用名字叫my-app。启动后执行pm2 list你能看到类似这样的状态┌─────┬────────┬─────────────┬─────────┬─────────┬──────────┐ │ id │ name │ namespace │ version │ mode │ status │ ├─────┼────────┼─────────────┼─────────┼─────────┼──────────┤ │ 0 │ my-app │ default │ 1.0.0 │ fork │ online │ └─────┴────────┴─────────────┴─────────┴─────────┴──────────┘启动后应用就脱离了终端。你可以直接exit退出SSH应用依然运行。--name这个参数非常重要尤其是当你管理多个应用时。没有名字的话PM2会用脚本文件路径作为名字看起来非常混乱。我通常在pm2 start时就指定一个清晰的名字比如shop-api、cron-worker。如果你有多个环境开发、测试、生产可以用--env参数配合配置文件来区分。3.2 配置文件ecosystem.config.js字段解析用命令启动适合临时试试规范的项目都会创建ecosystem.config.js。一个典型的配置长这样module.exports { apps: [ { name: web-api, script: ./src/index.js, instances: 2, exec_mode: cluster, watch: false, max_memory_restart: 500M, env: { NODE_ENV: production, PORT: 8080 }, env_dev: { NODE_ENV: development, PORT: 3000 }, log_file: ./logs/web-api.log, out_file: ./logs/out.log, error_file: ./logs/error.log, merge_logs: true, time: true } ] };这里每个字段都有讲究script: 入口文件的路径相对于当前shell的工作目录。这个和cwd字段配合使用后面坑部分会细说。instances: 实例数。可以是数字也可以是max表示根据CPU核数启动。exec_mode: 启动模式fork或者cluster。如果设了instances大于1必须配合cluster模式。watch: 设置为true时PM2会监听文件变化自动重启。生产环境一般不开开发环境可以开。max_memory_restart: 内存超过这个值就自动重启。这是防内存泄漏的重要手段。env: 环境变量会应用到所有环境。env_dev则可以在启动时用--env dev指定此时env_dev里的变量会覆盖env里的同名变量。out_file和error_file: 分别指定标准输出和错误日志的存放位置。如果不设PM2默认放~/.pm2/logs/。time: 日志行前面加上时间戳强烈建议打开。3.3 使用配置文件启动和查看状态执行pm2 start ecosystem.config.js如果有多个app可以用pm2 start ecosystem.config.js --only web-api只启动其中一个。启动以后用pm2 status或pm2 list看状态。这个命令输出的信息包括进程ID、状态、重启次数、CPU占用、内存占用、运行时间等是日常运维第一个看的命令。如果发现状态是errored或者stopped立刻看日志pm2 logs web-api --err只显示错误输出。如果要看实时输出pm2 logs web-api会进入一个实时跟随模式按CtrlC退出不会影响应用。这里有个小细节pm2 logs会同时显示out和error日志但如果在配置文件里把out_file和error_file设置成了同一个文件可能会混在一起。我通常让它们分开写方便排查。3.4 常用操作命令速查| 命令 | 作用 | |------|------| | pm2 restart web-api | 重启应用 | | pm2 reload web-api | 零停机重载cluster模式逐个重启worker | | pm2 stop web-api | 停止应用但保留进程记录 | | pm2 delete web-api | 删除应用记录彻底从PM2移除 | | pm2 save | 保存当前进程列表用于开机自启恢复 | | pm2 startup | 生成开机自启脚本 | | pm2 ls | 查看状态 | | pm2 monit | 进入实时监控面板 | | 记住这些命令就够了后面我们逐个说说生产环境怎么用。 # 4. PM2生产环境常用功能负载均衡、零停机重启与开机自启 ### 4.1 cluster模式与负载均衡 先说负载均衡。Node.js默认是单线程的即使服务器是16核CPU一个Node进程也只能用一核。要想充分利用CPU就必须启动多个进程。PM2的cluster模式把这件事简化成了配置里的一句话 bash pm2 start app.js -i max --name web-api-i max表示创建与CPU核数相等数量的worker进程。你也可以用-i 4指定4个。但是要注意cluster模式的负载均衡策略是Node内置的round-robin它只适用于HTTP服务。如果你启动的是一个WebSocket服务或者长连接服务多个worker进程分布式接收连接时需要处理连接状态同步的问题。很多直播类、消息推送类的应用宁可用单进程fork模式配合Nginx做多实例负载均衡也不要为了省事强开cluster。原理上cluster模式适合无状态或者状态可共享的服务有状态服务请慎用。还有一点cluster模式下每个worker进程都会listen同一个端口不要在你的代码里手动app.listen(PORT)之前判断是不是master进程否则会报端口冲突。PM2已经帮你处理好了你的代码只要正常写app.listen(port)就行。4.2 reload与restart一字之差天壤之别restart和reload的区别很多人一直没真正搞懂。restart是粗暴地杀掉进程再拉起这个过程会有一小段时间服务不可用。reload是PM2逐个worker重启先启动一个新的worker再把请求切换到新worker然后关掉旧的worker逐个轮替。这样整个服务始终有worker在响应请求实现了零停机更新。用法pm2 reload web-api前提是该应用是以cluster模式启动的。如果是fork模式单进程reload和restart效果几乎一样也会出现短暂中断。我在部署更新时通常的流程是在项目目录把新代码拉下来然后执行pm2 reload web-api。如果reload因为某种原因失败了PM2会自动回滚到旧进程所以比restart安全得多。restart一般用在哪里呢当你改了PM2的配置比如环境变量、端口、内存限制这时候需要用restart让整个进程读取新配置。因为reload只替换worker不会重新读取daemon层面的应用配置。很多人改了ecosystem.config.js里的环境变量敲了一个pm2 reload结果环境变量没生效还以为自己写错了其实就是命令用错了。4.3 pm2 startup让服务器重启后自动拉起所有进程服务器宕机、迁移、重启是运维永远的痛。有了PM2的startup这种痛苦可以减少一大半。操作分两步第一步在服务器上执行pm2 startup执行后PM2会输出一条类似sudo env PATH$PATH:/usr/bin pm2 startup systemd -u youruser --hp /home/youruser的命令你需要复制它并带sudo执行。这步的最终目的是在systemd里注册一个PM2的服务让系统开机时自动启动PM2 daemon。第二步保存当前进程列表pm2 savepm2 save会把当前PM2里所有的应用记录保存到~/.pm2/dump.pm2。这样开机时systemd先拉起PM2 daemonPM2 daemon再从dump文件里恢复所有进程。很多人在第一步就直接运行了没加sudo的pm2 startup然后发现没反应其实是PM2提示你需要那行带sudo的完整命令。注意执行完pm2 startup systemd之后还要看输出里有没有提示你什么权限问题通常是需要指定-u和--hp的当前用户。跟着提示做就行。配好以后可以用pm2 unstartup删除开机自启设置这是后话。5. 日志管理与内存监控PM2的运维能力5.1 日志分流、切割与日志轮转PM2默认把所有应用的日志写到~/.pm2/logs/下文件会越来越大。不处理的话半年以后一个日志文件能到几个GB排查问题的时候打开文件都能把编辑器卡死。PM2官方推荐用pm2-logrotate模块来自动切割日志。安装pm2 install pm2-logrotate该模块默认每天切割一次保留10份。你可以调整配置pm2 set pm2-logrotate:max_size 100M pm2 set pm2-logrotate:retain 7 pm2 set pm2-logrotate:compress truemax_size表示日志超过100M就切割retain表示保留7份compress表示对旧日志进行gzip压缩。这些设置足够应对绝大多数场景。另外你的应用代码里如果用console.log输出所有日志PM2会把它们作为标准输出。生产环境我建议至少区分console.log业务信息和console.error错误信息这样在pm2 logs --err里能单独看到错误。如果你的业务日志量很大还是建议引入winston或pino这样的日志框架把请求日志、关键操作日志写到固定的业务日志文件不要让PM2的日志机制承载所有日志。5.2 内存监控、自动重启和异常状态pm2 monit是PM2自带的简易监控面板进去以后能看到每个应用进程的CPU占用、内存占用、日志实时输出。不过说实话这个界面比较朴素适合在服务器上临时看。如果是多台服务器还是应该接上独立监控系统比如PrometheusGrafana或者云平台的监控。PM2更实用的能力是内存阈值自动重启。在配置里设置{ max_memory_restart: 500M }当某个worker进程内存超过500MPM2会自动重启它避免内存泄漏导致整个服务不可用。这里有个经验不要把阈值设得太低否则正常业务峰值一上来就触发重启反而会影响服务。一般设成正常峰值内存的1.5倍左右比较合适。比如你观察应用日常内存占用稳定在300M左右那阈值设成500M留出弹性空间。PM2重启是有次数限制的。如果一个进程反复崩溃PM2会在短时间内的重启次数超过阈值后把应用标记为errored不再无限重启。这个阈值是15秒内重启超过16次默认值。这是保护机制避免应用崩了以后无限循环重启把服务器拖垮。如果你看到应用状态是errored不要急着restart先看日志搞清崩溃原因。5.3 环境变量管理与配置动态生效环境变量用PM2配置比你在shell里export方便得多。最典型的场景是不同环境用不同的配置pm2 start ecosystem.config.js --env production注意--env参数只会在配置文件中匹配env_production字段而不会匹配env。很多人困惑为什么指定了--env productionNODE_ENV还是没变就是因为忘了还有一个底层的env字段会覆盖它。我推荐的做法是在ecosystem.config.js里基础变量放env中环境覆盖变量放在env_production、env_dev等二级对象中。启动时PM2会合并env和env_production后者优先。这样配置结构清晰不容易踩坑。如果你需要在运行中临时修改某个应用的环境变量最可靠的方式是pm2 restart web-api --update-env--update-env会让PM2重新读取当前shell的环境变量并把它们传给你的应用。它不会重新读取配置文件。所以如果你改了配置文件字段还是得用pm2 restart web-api且不带--update-env来做到。6. PM2踩坑实录从部署到稳定运行的真实教训6.1 环境变量NODE_ENV没生效的坑这是一个高频坑。很多人用NODE_ENVproduction pm2 start app.js心想环境变量已经传进去了结果应用里打印出来的还是development。原因是PM2 fork模式启动应用时它继承了PM2 daemon的环境变量并不是当前shell的环境变量。你在shell里临时设置的NODE_ENV不会自动传给PM2 daemondaemon只保留它自己启动时的环境。正确做法有三种第一种在ecosystem.config.js里配置env: { NODE_ENV: production }第二种用pm2 start app.js --env production并配合配置文件中的env_production。第三种命令行传递NODE_ENVproduction pm2 start app.js但你得在启动PM2 daemon之前设置这个环境变量当PM2 daemon已经是旧环境时推荐用pm2 restart web-api --update-env。我自己的习惯统一使用ecosystem.config.js不要混合使用多种方式。配置一旦混乱排查环境问题会非常痛苦。6.2 配置文件里的cwd与script路径PM2在启动应用时会以当前进程的工作目录CWD为基础解析script路径。如果你在服务器上从项目根目录执行pm2 start ecosystem.config.js那是没问题的。但如果你在/home/user目录下执行pm2 start /var/www/myapp/ecosystem.config.jsPM2不会自动把工作目录切到应用目录你的script: ./src/index.js就找不到文件了。解决方式是使用cwd字段{ name: web-api, cwd: /var/www/myapp, script: ./src/index.js }cwd表示当前工作目录建议在配置中始终显式指定cwd。这样无论你在哪个目录下启动都能正确找到脚本和相对路径资源。同样如果你的应用需要使用项目下的.env文件也要注意cwd。比如dotenv默认读取的是当前工作目录下的.env如果你没设置cwd而在别的地方启动就会读错文件。6.3 端口占用与cluster模式下的路径问题在cluster模式下多个worker共用同一个端口如果某个worker没有正确退出新worker启动时可能遇到端口被占用。PM2正常reload不会出现这个问题但如果你手动kill了一个worker进程PM2再重启时会可能碰到EADDRINUSE。这时候你先用pm2 delete把应用记录删掉然后重新pm2 start通常能解决。还有一个很多人忽略的坑cluster模式下的每个worker都会执行你的入口脚本。如果你的入口脚本里有初始化任务比如启动时创建数据库表、清空缓存那么这些任务会被执行多次。解决方案就是让初始化逻辑只在主进程执行worker里跳过。在PM2 cluster模式下可以用process.env.NODE_APP_INSTANCE判断是否是第一个workerif (process.env.NODE_APP_INSTANCE 0) { // 只在第一个worker里执行初始化 init(); }6.4 使用PM2维护Node服务的一点感想用了一段时间PM2后我的最大感受是它并没有解决所有运维问题很多它提供的自动功能需要你理解之后才能正确地使用。比如自动重启是好功能但如果你从不看日志那么每次自动重启都是在掩盖问题而不是解决问题。内存限制、日志分割、开机自启这些都是辅助工具真正的核心还是要你把应用本身做好同时借助PM2把进程生命周期管理起来。建议你把PM2的命令和配置固化在项目的README里甚至提供一份deploy.md部署文档。当团队里有新同学接手时照着文档就能完成一次标准的发布流程。我的标准流程通常是拉取代码 -npm install --production-pm2 restart web-api --update-env或pm2 reload- 查看状态和日志 - 跑一遍健康检查接口。最后再说一个日常小技巧如果你发现pm2 list里某个应用的重启次数异常增加不要只看最新日志应该先执行pm2 describe web-api查看这个应用的完整历史信息包括重启的时间点、退出码、执行路径。这个命令比单纯看日志更能帮你快速定位问题发生的规律。PM2本身就是一套运维语言花半小时把核心命令和配置弄明白省下来的可是一个又一个加班的深夜。
返回列表