讲成功案例谁都会讲。把系统出过的故障量化成方法论,才证明你真在生产环境待过。
只读体检发现:124.222.108.196 上的 LumenX 有 lumenx-backend / lumenx-mv-adapter / lumenx-tts 三个 pm2 进程,其中 lumenx-backend 近 2 天重启 482 次(约每 6 分钟一次)。这就是「稳定性隐患」的量级——不是感觉,是数出来的。
另一类难查故障:PM2 守护进程雪崩 + 孤儿进程(PPID=1),端口被孤儿占用导致 address already in use 崩溃重启环。⚠️ 排查纪律:先定位再杀,别无脑重启——重启会毁掉现场。
延伸:深入 17 · 进程模型与锁 讲清 PM2 多进程同时访问 SQLite 的写竞争问题。
这一篇我不想从「我做过什么」开始讲,从「我查出了什么」开始。因为负面事实比成功故事更能证明专业性——吹谁都会,查出病根才是本事。
LumenX 平台部署在 124.222.108.196,用 PM2 托管三个进程:
| 进程 | 职责 |
|---|---|
lumenx-backend | 主服务:剧本、分镜、资产、出片编排 |
lumenx-mv-adapter | 图像/视频模型适配层 |
lumenx-tts | 配音合成网关 |
因为它在监控上呈现为「进程是活的」。PM2 会自动把崩掉的进程拉起来,进程列表里三个都是 online,随手点开每个都是 green 状态。业务早就断了,监控显示一切正常。这直接引出本模块的核心观点——只盯存活层等于没监控(第 04 节展开)。
体检不是随便看看。有一套固定动作,四条命令覆盖 80% 的线上问题:
# ① 进程状态:重启次数、运行时长、内存 pm2 list pm2 describe lumenx-backend # ② 崩溃前最后 200 行日志(关键!崩溃原因都在这里) pm2 logs lumenx-backend --lines 200 --nostream # ③ 内核有没有 OOM 杀进程(JS 报错之外的死法) dmesg -T | tail -50 # ④ 内存与磁盘水位 free -m df -h
| 检查项 | 命令 | 看什么 | 报警阈值 |
|---|---|---|---|
| 重启次数 | pm2 list | restart 列是否异常增长。稳定进程应该是个位数或 0;482 次/2 天就是明确的病征 | 1 小时内 > 3 次 |
| 运行时长 | pm2 describe | uptime。刚启动就几分钟 ⇒ 崩溃重启环;长期稳定 ⇒ 正常 | uptime < 10 分钟且反复 |
| 内存 | free -m | used 占比、是否持续上涨不回落(典型内存泄漏信号);available 还有多少 | > 80% |
| 磁盘 | df -h | 出片类系统日志+产物增长极快。水位到85% 会连带写库失败 | > 85% |
| 崩溃原因 | pm2 logs --lines 200 | 最后一次退出的堆栈。是 OOM(JavaScript heap out of memory)、未捕获异常,还是被外部 kill | — |
| 内核杀进程 | dmesg -T | tail -50 | 搜 Out of memory / Killed process。JS 侧没报错但进程没了,通常是内核干的 | 出现即报警 |
比崩溃重启更让人头疼的,是PM2 自己失效、而应用全变成孤儿进程的状态。这时候表面上「进程都在」,实际上已经没有任何人管它们了。
God Daemon(PM2 主进程)挂了或被反复杀掉。PM2 的价值就在于「进程崩了自动拉起」,它自己一死,这个能力就没了。
所有应用进程的父进程变成 1(init/systemd),意味着它们脱离了PM2 监管。PM2 list 里可能是一片空白,但系统里进程多得像刷屏。
几十个 God Daemon 加上无人监管的应用进程一起吃内存,机器开始变慢、开始 swap。
address already in use你想重启应用,PM2 拉起新进程 → 端口被那个还活着的孤儿占着 → 新进程起来就崩 → 形成崩溃重启环。这一步最有迷惑性:看起来像代码 bug,实际是环境问题。
# ① 先找孤儿:父进程为 1 的进程 ps -eo pid,ppid,cmd | awk '$2==1' # ② 看它们占了多少内存(几十个的话这里就是元凶) ps -eo pid,ppid,rss,etime,cmd --sort=-rss | head -30 # ③ 确认端口被谁占了(对应报错里的端口号) ss -lntp | grep 3000 lsof -i :3000 # ④ 先杀孤儿、但不要急着重启 —— 先取证! # 此刻 pm2 logs 还留着最后几行日志,这是唯一的现场 # ⑤ 现场留够了,再恢复 PM2 托管 pm2 resurrect pm2 save
address already in use 就条件反射重启 —— 每次重启都把崩溃前的现场清空了,查一周都查不出根因。至少先把 pm2 logs 尾部导出来。address already in use 是个非常「像代码问题」的错误。绝大多数人会去查代码、查配置、改端口号 —— 全是白费,因为根因是进程归属关系断了。这个坑只有真正在生产上处理过的人才知道。监控不是装个探针就完事。关键是要分清三层,并且知道每层能证明什么、不能证明什么。
| 层级 | 指标 | 能证明什么 | 不能证明什么 |
|---|---|---|---|
| 存活层 | 进程是否在、重启次数、内存/磁盘水位、主机负载 | 进程还活着没、资源还够用 | 不能证明业务能用。LumenX 那个案例就死在这层——进程明明「在线」,但每6 分钟重启一次,业务早就断了 |
| 健康层 | HTTP 返回码、响应时间 P95、关键任务成功率、依赖上游是否可达 | 接口真的能返回正确结果,不是超时就是 500 | 不能证明业务量正常。接口全200 但一单没出,也是"健康" |
| 业务层 | 单据产生量、失败单数、队列积压量、关键动作漏斗 | 用户真正感知到的东西——今天到底有没有在出活 | 不能定位根因。业务掉了你还得往下钻到健康层、存活层 |
业务层掉单 → 顺着健康层查是哪个接口超时 → 再顺着存活层查是内存爆了还是进程在重启环。三层是自上而下钻的路径,不是三个独立的图。做监控规划时,从业务层往下倒推,比从 CPU 往上堆要靠谱得多。
告警的唯一价值是让人去做一件事。不产生动作的告警,只是半夜的噪声。
| 类型 | 触发逻辑 | 例子 | 适合什么 |
|---|---|---|---|
| 阈值型 | 指标越过固定线 | 内存 > 80%、磁盘 > 85%、P95 > 3 秒、1 小时重启 > 3 次 | 资源类指标,波动小的 |
| 变化率型 | 相对自身历史基线突变 | 单据量比上周同时段跌 60%、错误率从 0.1% 跳到 8% | 业务量指标,绝对值没意义时 |
| 心跳缺失型 | 该来的信号没来 | 健康检查连续 3 次失败、每5 分钟应上报一次的指标没上报、日报没生成 | 最难被绕过的一类——业务断了但机器没死,只有心跳能发现 |
这一节容易被当成「财务问题」,但对出片类系统来说,成本失控和宕机是同一种故障——都会让项目停摆。
LumenX 依赖 6 个以上付费外部 API:
这类调用的特点是:出片即烧钱,且失败重试也会烧钱。而原先这层是没有可见成本护栏的——意思是没人能在事发前知道今天花了多少,也没人能拦住「一个死循环任务连续调一万次视频接口」。
| 护栏 | 怎么做 | 解决什么问题 |
|---|---|---|
| 日调用上限 | 按接口维度设日配额,超限直接拒 | 止住异常情况下的无限烧钱 |
| 超额熔断 | 达到阈值后切到降级通道并告警,不继续重试 | 重试风暴是成本爆炸的头号原因 |
| 缓存复用 | 相同参数的结果缓存,重复请求不重复计费 | 重试优先命中缓存而不是重打一次 |
| 降级到便宜模型 | 预算或积压超限时,自动切低成本通道出草稿 | 保住「能出活」这个底线,而不是直接瘫 |
| 用量日报 | 按接口/按日/按业务方出用量与成本表 | 没有可见性就没有护栏——看不见的花费一定失控 |
不追求「永不出事」,追求出事能在几分钟内止血。止损和定位是两件事:先止住,再慢慢查。
| 什么情况 | 止损动作 | 事后复盘 |
|---|---|---|
| 新版本上线后错误率飙升 | 立刻回滚到上一版,不带犹豫 | 回滚后保留故障现场(不在生产机上查),离线复现 |
| 成本异常飙升 / 调用量异常 | 先熔断上游接口,再查任务队列 | 核对是重试放大、任务死循环,还是配置被改 |
| 磁盘写满,数据库写入失败 | 清产物与历史日志,必要时临时扩盘 | 定产物/日志保留周期,加水位告警 |
| 内存持续上涨打满 | 重启该进程止血,重启前先导出日志 | 查是否有未释放的大对象/ 未关闭的连接池 |
| 进程崩溃重启环(如 6 分钟一次) | 先摘流量/停调度,再排查根因,不要靠无限重启续命 | 按第 02 节四件套定位,根因不清不许恢复调度 |
| 数据写错(脏数据入库) | 停写入,回滚数据/切只读,数据回退是单向门,必须有备份 | 补校验规则和前置检查,不靠人工肉眼兜底 |
| 坑 | 为什么危险 | 正确做法 |
|---|---|---|
| 只重启不定位 | 重启销毁现场,根因永远查不出来,同类故障反复发生 | 重启前先 pm2 logs --lines 200 导日志,再 dmesg -T | tail -50 |
| 看日志只看最后几行 | 崩溃原因常在更早的堆栈里,最后几行可能只是后果 | 看 200 行起步,往前找到第一个 Error |
| 用「应该没问题」代替监控 | 「应该」是所有事故的共同前因 | 没有数字就没有结论。2 天 482 次是事实,「应该没问题」不是 |
| 告警没人响应 | 没责任人的告警会被习惯性静音,等于没设 | 每条告警带责任人和明确动作,按级别区分是否叫醒 |
| 告警风暴 | 一次报几百条 ⇒ 关键告警被淹没(告警疲劳) | 收敛优先:一条根因告警 + 若干聚合项 |
| 成本无上限 | 出片即烧钱,重试会放大一个数量级 | 日配额 + 熔断 + 缓存 + 降级 + 用量日报,五件套一起上 |
| 把 PM2 重启当「自动恢复」 | 自动拉起掩盖了问题,业务在反复中断而没人知道 | 重启次数本身就是告警指标,超过阈值要人介入 |
| 在生产机上直接改配置试 | 改坏没人知道,且和故障现场混在一起 | 先在预览/只读环境验证,生产变更前先备份 + 留回滚路径 |
pm2 list / describe / logs --lines 200 / dmesg -T / free -m / df -h 各自看什么ps -eo pid,ppid,cmd | awk '$2==1' 找出孤儿进程address already in use 可能是孤儿进程占端口,而不是代码 bug我先讲一个我自己查出来的真实问题,因为我觉得它比讲成功案例更能说明我懂生产环境。
我给LumenX 平台(124.222.108.196,PM2 托管 lumenx-backend、lumenx-mv-adapter、lumenx-tts 三个进程)做了一次只读体检,发现 lumenx-backend 近2 天内重启了 482 次,平均大约每 6 分钟崩一次。但如果只看进程列表,三个进程全是 online 状态,看上去一切正常——因为 PM2 会自动把崩掉的进程拉起来。这就是我最大的一个认知收获:只盯存活层等于没监控。
我的排查动作很固定,也刻意保持只读:先用 pm2 list 和 pm2 describe 看重启计数和 uptime,确认是崩溃重启环而不是偶发;再用 pm2 logs --lines 200 看崩溃前最后的报错,区分是JS 堆溢出还是未捕获异常;然后 dmesg -T | tail -50 看内核有没有 Out of memory / Killed process;最后 free -m 和 df -h 看内存和磁盘水位。内存超 80%、磁盘超 85% 就该报警,1 小时重启超过 3 次就该人介入。
还有一类更难查的:PM2 守护进程本身失效,应用全变成孤儿进程,PPID=1 脱离监管,同时机器内存被几十个进程吃光。这时新进程起来会报 address already in use——因为端口被还活着的孤儿占着,形成崩溃重启环。查法是 ps -eo pid,ppid,cmd | awk '$2==1' 找 PPID=1 的进程,配合 ss -lntp 确认端口归属。这种错最容易被误判成代码 bug,其实根因是进程归属关系断了。
这里有一条我踩过、也写进纪律的原则:先定位、再杀进程、最后才重启。因为重启会抹掉唯一的现场——崩溃前的日志、内存曲线、孤儿进程列表都是一次性的。毁掉现场,一个五分钟能查清的问题会变成一周都查不清。所以那次体检我没有动生产系统任何配置、没有重启任何进程,全流程只读取证;生产变更前的原则也是一样的:先备份、留回滚路径、在预览环境验证过再上。
监控层面我分三层:存活层(进程、重启次数、水位)只能证明进程还活着;健康层(返回码、P95、任务成功率)证明接口真能用;业务层(单据产生量、失败单数、队列积压)才证明用户真的在用。LumenX 那个案例正好死在第一层。告警上我优先做心跳缺失型,因为它的失败是静默的——进程在、接口甚至还返 200,但业务早断了。告警必须有人看、必须有明确动作,还要防告警疲劳,一次报几百条等于没报。
最后一点是我认为被低估的:成本护栏也是稳定性的一部分。LumenX 依赖 6 个以上付费外部 API(DashScope、ARK、Kling、Vidu、ModelVerse、OpenAI,加火山 TTS 和 OSS 存储),出片即烧钱,而原先没有可见的护栏。我会配日调用上限、超额熔断、缓存复用、降级到便宜模型和用量日报——重试是最贵的故障模式,一个本该失败的任务重试几次,配上视频模型单价,一晚上能烧掉一个月预算。不崩但成本失控,系统照样会被叫停。