pm2线上排障实战:进程状态、重启与日志的正确顺序
凌晨两点被告警叫醒网关上一片 502登机器pm2 list一看那个进程状态明明写着 online可接口就是不通。这种时候能不能把手上的 pm2 查看、重启服务和日志这三件事按正确顺序做一遍基本决定了你是在电脑前坐十分钟还是耗到天亮。我做过几年 Node 服务的线上维护pm2 这种进程管理工具几乎每个项目都会用到但真正把它的状态视图、重启语义和日志机制摸清楚的人并不多大部分人只记住了pm2 restart和pm2 logs两条命令出了事就开始瞎试。这篇东西面向的是已经在用 pm2 管服务、但遇到问题只能靠重启赌运气的同学也适合刚接手一台跑着 pm2 的服务器、想快速建立排查手感的运维和全栈开发。下面这些内容基本都是我在真机上反复验证过的命令可以直接抄坑也标出来了。1. 进程状态怎么看pm2 list 之外的三个视角1.1 pm2 list 那张表里真正值得盯的几列pm2 list、pm2 ls、pm2 status三条命令等价输出的表格看着信息很多实际排障时只有四列是有价值的status、↺、uptime、mem。status的取值比大多数人想的要多除了常见的online和stopped还有launching、stopping、errored、one-launch-status。errored尤其要警惕它意味着进程反复崩溃后已经被 pm2 放弃不再尝试拉起这时候你restart一次它会短暂上线再变回errored真正的信息在日志里而不在状态里。↺那一列是重启次数这是判断服务稳定性的第一指标。一个跑了一周的服务这个数字应该是 0 或者个位数。如果它每几分钟就往上跳一次说明进程在反复退出pm2 list里是看不出来原因的必须去查 error 日志。uptime和↺要结合着看。我遇到过 uptime 显示3m、重启次数217的情况那就是典型的崩溃循环。反过来如果 uptime 是12D、重启次数是 0但这个进程的mem在缓慢爬升那就是另一类问题——内存泄漏只是还没触发崩溃而已。列名实际含义排障时的判断价值id进程编号重启后可能变化脚本里不要用 id用 namename进程名默认取脚本文件名所有操作的推荐锚点modefork 或 cluster决定 reload 是否真的平滑status运行状态errored 说明已被放弃拉起↺重启累计次数快速上涨即崩溃循环uptime当前实例存活时长与 ↺ 结合判断稳定性mem常驻内存持续上涨即为泄漏信号提示pm2 list默认按 id 排序进程多的时候加--sort memory或--sort cpu更直观这两个参数在查内存泄漏时比翻日志快得多。1.2 pm2 describe 才是排障的主力pm2 describe name|id也可以写pm2 show打印的是单个进程的完整档案信息量大到第一次看会有点晕但里面至少有六项是必看的。第一项是script path和script args很多时候服务行为不对是因为启动的压根不是你以为的那个文件或者参数里少了--envprod这类关键开关。第二项是exec cwd工作目录错了会导致相对路径读配置文件失败这类问题在日志里往往只体现为一句莫名其妙的ENOENT。第三项是exec mode它会明确告诉你这个进程是 fork 还是 cluster直接决定第五节要讲的 reload 行为。第四项是unstable restarts这个计数器专门统计那些启动后不到min_uptime就退出的次数。它比总重启次数更能反映配置问题因为只要应用能稳定跑够时间这个数字就不会动。第五项是exit code进程最后一次退出的返回码。137 基本等于被系统强杀1289多见于内存超限1 是应用自己抛异常退出0 通常意味着进程是被正常信号终止的比如你手动 stop 之后再也没起来。第六项是node args和watch生产环境里watch必须是关闭的这个开关一旦打开你改任何一个被监听的文件都会触发重启重启计数器会以你想象不到的速度上涨。pm2 env id是 describe 的补充它把该进程实际继承的环境变量全部打印出来。改过.env文件但服务行为没变的情况八成就是这个进程还挂着旧的环境变量需要带--update-env重启。1.3 pm2 monit 与机器层面的对照pm2 monit是个全屏的交互式面板左边进程列表、右边实时日志单机排障时很好用。但它有两个明显限制一是不能用管道和脚本处理二是它显示的 CPU 和内存是 pm2 自己采样的粒度和系统层面有差异。我的习惯是 monit 看趋势top或htop看细节。当 monit 里某个进程的 mem 一直贴着上限走就切到系统层面用ps aux --sort-%mem | head确认一下再对比pm2 describe里的max memory restart阈值就能判断这个进程距离被自动重启还有多远。还有一个容易被忽略的点pm2 monit显示的日志是内存里缓存的近期输出不是文件里的完整内容。进程重启之后之前那段输出就从 monit 里消失了所以真正定责的时候必须以日志文件为准。2. 重启的三种语义restart、reload、stop 再 start 到底差在哪2.1 restart 是硬重启reload 才是平滑pm2 restart name的动作很直接向进程发信号终止等它退出后重新拉起一个。这个过程中服务是不可用的哪怕只有几百毫秒对于有连接保持的客户端来说就是一次断连。如果你配置了kill_timeout默认 1600ms进程没在这个窗口内退出pm2 会直接上 SIGKILL应用里注册的优雅退出逻辑就完全没机会执行。pm2 reload name是另一套逻辑它只在 cluster 模式下有意义。cluster 模式下同一个应用有多个 worker 实例reload 会逐个替换先起一个新 worker等它 ready 之后再干掉一个旧的循环直到全部换完。整个过程理论上没有服务中断窗口。这里有个非常常见的误解在 fork 模式下执行pm2 reload并不会平滑因为只有一个进程pm2 没有腾挪的余地实际效果和 restart 差不多。所以如果你的应用跑的是 fork 模式指望reload实现零停机是徒劳的要么切到 cluster 模式要么接受重启时的短暂抖动。判断自己是不是 cluster 模式pm2 describe里的exec mode一看便知或者pm2 list里 mode 那一列写的是cluster还是fork。2.2 单进程、批量与环境变量刷新最常用的三条形式pm2 restart api-server # 重启单个按名字 pm2 restart 3 # 按 id 重启id 会变慎用 pm2 restart all # 全部重启会挨个来不是并行的如果这次重启是为了让新改的环境变量或配置文件生效一定要带上--update-envpm2 restart api-server --update-env不带这个参数pm2 会复用进程创建时记录的那份环境变量快照你改的.env或者 shell 里 export 的新值根本进不去。这个坑我在项目里见过不止一次表现是改完配置重启了但服务行为完全没变查半天最后发现就差一个参数。批量操作里还有两个细节值得说。一是pm2 restart all是按顺序逐个处理的如果你的服务之间有依赖关系比如网关依赖后端最好还是按名字分两条命令把启动顺序控制在自己手里。二是重启的时间点可以在配置里挂cron_restart做定时滚动重启比如每天凌晨三点cron_restart: 0 3 * * *这招对那种有轻微内存泄漏、一时半会儿修不掉的服务特别实用相当于用可控的重启替代不可控的崩溃。2.3 重启之后必须做的两件事save 与确认很多人重启完看到online就关掉终端了这里漏了两步。第一步是pm2 save。pm2 的进程列表是存在内存和一份 dump 文件里的只有执行pm2 save才会把当前列表写进~/.pm2/dump.pm2。如果你不保存机器重启之后 pm2 拉起的是上一次保存的列表你以为已经在跑的新进程压根不在其中。第二步是确认重启计数和 uptime。pm2 list看到↺从 N 变成 N1、uptime 归零说明这次重启确实生效了。如果 uptime 一直不涨或者↺在几分钟内又跳了几次那就不是重启能解决的问题得回到日志里找原因——这一点我后面会专门讲。另外如果重启后发现行为跟预期完全不符先别急着继续重启用pm2 describe确认一下exec cwd和script path有没有问题。服务重启后读到了另一份配置这种情况在有多套部署目录的机器上非常常见。3. 日志从哪来、怎么看、怎么不被它撑爆磁盘3.1 pm2 logs 的参数组合pm2 logs是实时 tail默认只显示最近 15 行然后持续跟随。这个默认值很多人不知道导致第一次用的人以为日志丢了。常用的参数组合我整理如下pm2 logs api-server # 只看某个进程out 和 err 混合 pm2 logs api-server --lines 200 # 先吐最后 200 行再跟随 pm2 logs api-server --err # 只看 stderr pm2 logs api-server --out # 只看 stdout pm2 logs --nostream # 打印完已有内容就退出不跟随 pm2 logs --raw # 去掉 pm2 加的进程名前缀原始输出 pm2 logs --timestamp # 每行前面加时间戳--nostream这个参数在写脚本或者把日志丢给别的工具处理时特别有用因为默认的跟随模式会让管道一直不返回脚本卡死在那里。--raw则适合做日志分析pm2 默认会在每行前面加上进程名、id 和方向标记虽然看着清楚但用grep提取结构化内容时会碍事。--timestamp加的是 pm2 收到这条输出时的时间戳和日志里应用自己打印的时间可能不一样。做前后端联调、排查超时问题时这个差别有时候很致命所以关键接口的日志里最好由应用自己带上精确时间。3.2 日志文件落盘位置与 out/error 的分工pm2 会把日志写到$PM2_HOME/logs下默认$PM2_HOME是当前用户的~/.pm2所以完整路径通常是~/.pm2/logs/app-name-out.log ~/.pm2/logs/app-name-error.log这个默认路径有几个需要注意的地方。第一$PM2_HOME是可以被环境变量覆盖的如果这个变量被改过日志就不在~/.pm2下面了可以用echo $PM2_HOME确认。第二如果用sudo启动过 pm2日志会跑到 root 的~/.pm2/logs里而当前用户pm2 list看到的可能是自己那份列表两边对不上。这种情况我建议直接用pm2 describe看实际的out log path和error log path别猜。关于 out 和 error 的分工也要说清楚console.log走 outconsole.error走 error但未捕获异常和进程崩溃的堆栈走的是 error。所以排查崩溃只看 out 日志是找不到东西的务必先看 error。另外日志是追加写的进程重启不会清空文件。这意味着你可以通过重启前最后一次启动的标记在日志里定位本次运行的所有输出这是排查崩溃循环时最有效的手法先pm2 flush name清空再pm2 logs name盯着看它到底吐了什么再退出。3.3 pm2-logrotate 的必要配置日志不清会要命。一个打印比较密的 Node 服务一天写几百兆很轻松~/.pm2/logs撑满分区之后系统上所有服务都会开始出问题。pm2 官方提供了 logrotate 模块装一次就行pm2 install pm2-logrotate pm2 set pm2-logrotate:max_size 20M pm2 set pm2-logrotate:retain 14 pm2 set pm2-logrotate:compress true pm2 set pm2-logrotate:rotateInterval 0 0 * * * pm2 set pm2-logrotate:workerInterval 30max_size是单文件上限超过就切retain是保留的历史文件个数不设或者设成 0 会无限保留等于没解决问题compress打开之后历史文件会被 gzip磁盘占用能降一个数量级rotateInterval是强制切割的时间点防止某些文件虽然没到大小上限但已经跑了很久。这里有个容易被忘掉的步骤这些设置是存在 pm2 模块自己的配置里的换机器或者重建环境时需要重新执行一遍所以最好把它们写进你的部署脚本。有次我迁移服务器代码和配置全搬过去了唯独忘了 logrotate 的设置一周之后磁盘告警才想起来。3.4 用 shell 组合拳定位具体一次报错日志文件到手之后真实场景下我们要找的往往是最近一次崩溃前发生了什么。下面这套组合我用了很多次效率很高# 1. 看错误日志的尾部找崩溃堆栈 tail -n 300 ~/.pm2/logs/api-server-error.log # 2. 在超大文件里追某个关键词只看最后 50 条匹配 grep -n ECONNREFUSED ~/.pm2/logs/api-server-error.log | tail -n 50 # 3. 找到行号之后看上下文前后各 20 行 sed -n 10240,10280p ~/.pm2/logs/api-server-error.log # 4. 统计最近一小时哪类错误最多 grep $(date -d 1 hour ago %Y-%m-%d %H) ~/.pm2/logs/api-server-error.log \ | awk {print $NF} | sort | uniq -c | sort -rn | head最后一条依赖日志格式固定如果你的日志是多行堆栈效果会打折。所以我在项目里会要求关键错误用单行结构化格式打印比如 JSON 一行一条这样grep和awk才有的放矢。多行堆栈留给进程级别的未捕获异常那种本来就该人工看。4. 四种高频故障的重启与日志排查链路4.1 进程 online 但接口不通先分清是假死还是没接住流量这种情况我遇到过两次两次原因完全不同。第一次是应用的事件循环被一个同步操作阻塞了。进程确实活着pm2 list显示onlineuptime 也在涨但所有请求都超时。判断方法很简单pm2 monit看 CPU如果某个进程 CPU 长期贴着 100% 而请求量并不大基本就是同步阻塞或者死循环。这种问题重启能恢复但根因得靠代码里的慢操作排查日志里通常什么都看不到。第二次是 cluster 模式下部分 worker 没接住流量。pm2 list显示的是一行但实际背后有好几个实例其中一两个卡死时整体状态还是 online。这时候pm2 describe里的实例数量和pm2 list里的 mode 就派上用场了用pm2 reload逐个替换可以把卡死的实例换掉且不影响其他实例。判断顺序建议是这样先pm2 describe确认实例数和 mode再pm2 monit看 CPU 和内存趋势最后才考虑重启。先重启再排查是排障里最容易犯的错因为重启之后现场就没了你得等它下次复发。4.2 EADDRINUSE端口没释放导致的无限重启错误日志里出现EADDRINUSE: address already in use :::3000然后进程退出、pm2 拉起、又失败形成崩溃循环重启计数飞快上涨。这时候pm2 restart一点用都没有因为问题不是进程状态而是端口被别人占着。排查要先确认端口归属ss -lntp | grep 3000 # 或者 lsof -i:3000常见原因是上一个进程还没完全退出新进程就起来了。这通常发生在你手动pm2 stop之后立刻pm2 start或者kill_timeout设得太短。kill_timeout默认大约是 1.6 秒如果你的应用关闭时需要清理连接池、等待正在处理的请求这个时间明显不够进程会被 SIGKILL 强杀而操作系统回收端口还需要一点时间新进程启动时就撞上了。解决办法是把kill_timeout调到 5000 到 10000 毫秒并在应用里正确处理SIGINT和SIGTERM信号主动关闭监听、等待未完成请求。这一步做完pm2 reload才能真的平滑否则再完美的配置也白搭。还有一种情况是别的服务占了这个端口比如你把测试环境的一个服务留在机器上。这种就只能换端口或者下线那个服务了pm2 层面无能为力。4.3 内存涨到阈值被系统杀掉exit code 137pm2 describe里exit code显示 137配合 error 日志里没有任何应用级堆栈基本可以断定是被 SIGKILL 掉了最常见的原因就是内存超限被系统的 OOM 机制选中。pm2 自身也提供了内存保护在启动参数里加pm2 start app.js --name api-server --max-memory-restart 800M或者在配置文件里写max_memory_restart: 800M。超过阈值时 pm2 会主动重启这个进程比等着系统 OOM 杀掉要体面得多至少日志里能看到 pm2 自己记录的这次重启。但这里要提醒一句设置内存上限是止血不是治病。如果↺每几小时就加一说明进程确实在涨内存得去查是不是有缓存没有过期策略、事件监听没解绑、或者流式处理没有正确关闭。我一般会把阈值设成稳定运行时内存的两倍左右既能兜住缓慢泄漏又不至于频繁误触发。判断泄漏的土办法连续几次pm2 describe看mem这一项的变化曲线。稳定服务的内存会在一个区间内波动泄漏服务则是阶梯式上升重启后才归零。4.4 cluster 模式下日志串行错乱cluster 模式下多个实例会写同一个日志文件日志行会互相穿插读起来像乱码。解决办法是打开merge_logs并在应用里用instance_var给每条日志打上实例标记merge_logs: true, instance_var: INSTANCE_ID配置之后应用里可以通过process.env.INSTANCE_ID拿到当前实例编号日志里带上它多实例场景下的问题就变得可追踪了。如果你更希望每个实例独立写文件也可以给每个实例配不同的out_file但那样日志就散了我不太推荐。还有一个相关坑多个实例同时向同一个文件追加写在某些文件系统上确实可能出现行被截断。pm2 的merge_logs用的是单进程写多实例日志的方式能规避大部分问题但最稳妥的做法还是在应用层面把日志按实例分文件或者直接输出到标准输出交给外部日志收集处理。5. 把配置固化下来ecosystem.config.js 与自启5.1 一份可以直接抄的配置命令行参数用多了迟早会忘正确做法是全部收进ecosystem.config.js用pm2 start ecosystem.config.js启动。下面这份是我在多数生产项目上用的模板参数基本覆盖了前面提到的所有要点module.exports { apps: [ { name: api-server, script: ./dist/main.js, cwd: /opt/apps/api-server, instances: 2, exec_mode: cluster, watch: false, max_memory_restart: 800M, min_uptime: 20s, max_restarts: 10, kill_timeout: 8000, listen_timeout: 10000, merge_logs: true, log_date_format: YYYY-MM-DD HH:mm:ss.SSS, instance_var: INSTANCE_ID, error_file: /var/log/pm2/api-error.log, out_file: /var/log/pm2/api-out.log, env: { NODE_ENV: production, PORT: 3000 }, env_staging: { NODE_ENV: staging, PORT: 3100 } } ] }几个参数的理由值得单独讲。min_uptime配合max_restarts组成熔断如果进程在 20 秒内就退出算一次不稳定重启连续 10 次之后 pm2 放弃拉起状态变成errored。这个机制防止了配置错误导致的无脑重启把机器资源吃干净同时errored状态本身就是一个非常明确的告警信号。listen_timeout配合应用里的process.send(ready)使用是 cluster 模式平滑重启的关键。应用真正初始化完成数据库连接建立、缓存预热完毕之后再通知 pm2这样 reload 时新实例是可以接客的不会出现进程在线但接口 500的空窗期。按环境启动用--env切换pm2 start ecosystem.config.js --env staging pm2 startOrReload ecosystem.config.js --env production --only api-serverstartOrReload是部署脚本里的常客它会把已存在的进程按 reload 的方式替换不存在则新建一条命令覆盖两种情况。5.2 开机自启的完整闭环自启这件事有三个步骤缺一不可而且顺序不能乱# 1. 生成开机启动脚本按提示执行它输出的那行 sudo 命令 pm2 startup systemd -u deploy --hp /home/deploy # 2. 用部署账号不是 root启动服务 pm2 start ecosystem.config.js --env production # 3. 保存当前进程列表 pm2 save最常见的失败模式是第 2 步用了sudo pm2 start。这样进程归 root 管dump 文件也写到 root 的~/.pm2下而 systemd 服务是以deploy用户运行的开机时它读的是/home/deploy/.pm2/dump.pm2里面空空如也。表现就是手动启动都正常一重启机器服务全没了。遇到这种情况先pm2 kill清掉 root 那边的守护进程再用正确的账号重来一遍。需要撤销自启时用pm2 unstartup手动恢复已保存的列表用pm2 resurrect这两个命令在排查自启问题时很有用。5.3 容器场景下不要用常驻守护模式如果 pm2 跑在容器里用它默认的守护进程模式会带来一个麻烦pm2 自己 fork 出一个后台进程容器的主进程 PID 1 就变成了这个守护进程信号传递和退出码处理都会变得别扭容器编排层发来的停止信号未必能正确传到业务进程。这种场景应该换成pm2-runtimepm2-runtime start ecosystem.config.js --env production它以非守护模式前台运行日志直接输出到标准输出正好符合容器的日志约定信号也能正常传递。代价是不能再交互式地用pm2 list、pm2 logs这些命令查看状态要靠容器外部工具。所以我的建议是传统虚机部署用 pm2 守护模式加pm2 save自启容器部署用pm2-runtime加编排层的重启策略两套思路不要混着用。下面这张表是我自己理出来的高频命令速查贴在服务器上随时可以对照需求命令备注看进程状态pm2 list重点看 status、↺、uptime看单个进程详情pm2 describe name关注 exit code、unstable restarts看环境变量pm2 env id改配置后没生效先查这里硬重启pm2 restart name有短暂中断平滑重启pm2 reload name仅 cluster 模式有效刷新环境变量重启pm2 restart name --update-env改完 .env 必带实时日志pm2 logs name --lines 200默认只显示 15 行只看错误日志pm2 logs name --err崩溃堆栈在这里清空日志pm2 flush name排查崩溃循环前先用保存列表pm2 save自启前必做重置重启计数pm2 reset name观察期重新计数看内存排序pm2 list --sort memory查泄漏常用最后再分享一个小技巧。排查那种一周崩一次的疑难问题时我会临时把max_restarts调低到 3 左右让进程在崩溃后尽快进入errored状态而不是无限重启然后把 error 日志切到独立文件并打开压缩。这样它一崩我打开日志看到的就是干净的一次运行记录不用在几千行重复内容里翻找。等根因定位完再把参数调回正常值。这个法子比开着pm2 logs蹲守靠谱得多毕竟没人能盯一个通宵等它崩。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →