FDE·交付知识库
目录
交付方法论
需求诊断 项目切片 灰度上线 验收交接 报价期望 集成治理
技术纵深
稳定性 稳定性 定价引擎
基础
①托管 ②SEO ③验证 ④AI产线

模块 12 · 稳定性与监控

讲成功案例谁都会讲。把系统出过的故障量化成方法论,才证明你真在生产环境待过。

项目锚点 · LumenX 近 2 天 pm2 重启 482 次

只读体检发现: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 的写竞争问题。

01

开篇一个真实反面案例

这一篇我不想从「我做过什么」开始讲,从「我查出了什么」开始。因为负面事实比成功故事更能证明专业性——吹谁都会,查出病根才是本事。

LumenX 平台部署在 124.222.108.196,用 PM2 托管三个进程:

进程职责
lumenx-backend主服务:剧本、分镜、资产、出片编排
lumenx-mv-adapter图像/视频模型适配层
lumenx-tts配音合成网关
做只读体检时发现:lumenx-backend 近 2 天内重启 482 次,平均约每 6 分钟崩溃一次。两个适配层进程反而是稳的。问题被精确定位到了主服务这一个进程上。
这个案例的价值在于「量级」。客户说「系统有点不稳」,这是感觉;2 天 482 次、约 6 分钟一次,这是事实。感觉没法立项,事实可以。

为什么这类问题容易漏掉

因为它在监控上呈现为「进程是活的」。PM2 会自动把崩掉的进程拉起来,进程列表里三个都是 online,随手点开每个都是 green 状态。业务早就断了,监控显示一切正常。这直接引出本模块的核心观点——只盯存活层等于没监控(第 04 节展开)。

纪律声明:这次排查全程只读。我没有动生产系统的一行配置、没有重启任何进程。原因是——你一旦重启,现场就没了。崩溃前的日志、内存曲线、孤儿进程列表,全都会被抹掉。先取证,再动手,这是生产环境的基本纪律。
02

怎么发现这类问题:只读排查四件套

体检不是随便看看。有一套固定动作,四条命令覆盖 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 listrestart 列是否异常增长。稳定进程应该是个位数或 0;482 次/2 天就是明确的病征1 小时内 > 3 次
运行时长pm2 describeuptime。刚启动就几分钟 ⇒ 崩溃重启环;长期稳定 ⇒ 正常uptime < 10 分钟且反复
内存free -mused 占比、是否持续上涨不回落(典型内存泄漏信号);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 侧没报错但进程没了,通常是内核干的出现即报警
只读体检的顺序别乱:先看数(重启次数、内存、磁盘),再看日志,最后看内核。因为一旦你动过任何东西,日志和现场就没了。取证在前,动手在后。
482 次重启的真正根因,要靠第③ 条日志 + 第 ⑥ 条 dmesg 才能定,不能靠猜 free -m 看 available 不看 free
03

另一种难查的故障:守护进程雪崩+ 孤儿进程

比崩溃重启更让人头疼的,是PM2 自己失效、而应用全变成孤儿进程的状态。这时候表面上「进程都在」,实际上已经没有任何人管它们了。

典型症状链

1

PM2 守护进程自身失效

God Daemon(PM2 主进程)挂了或被反复杀掉。PM2 的价值就在于「进程崩了自动拉起」,它自己一死,这个能力就没了。

2

应用变成孤儿进程:PPID=1

所有应用进程的父进程变成 1(init/systemd),意味着它们脱离了PM2 监管。PM2 list 里可能是一片空白,但系统里进程多得像刷屏。

3

内存被吃光

几十个 God Daemon 加上无人监管的应用进程一起吃内存,机器开始变慢、开始 swap。

4

端口被孤儿占用,报 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 是个非常「像代码问题」的错误。绝大多数人会去查代码、查配置、改端口号 —— 全是白费,因为根因是进程归属关系断了。这个坑只有真正在生产上处理过的人才知道。
04

监控该盯什么:三层缺一不可

监控不是装个探针就完事。关键是要分清三层,并且知道每层能证明什么、不能证明什么。

层级指标能证明什么不能证明什么
存活层进程是否在、重启次数、内存/磁盘水位、主机负载进程还活着没、资源还够用不能证明业务能用。LumenX 那个案例就死在这层——进程明明「在线」,但每6 分钟重启一次,业务早就断了
健康层HTTP 返回码、响应时间 P95、关键任务成功率、依赖上游是否可达接口真的能返回正确结果,不是超时就是 500不能证明业务量正常。接口全200 但一单没出,也是"健康"
业务层单据产生量、失败单数、队列积压量、关键动作漏斗用户真正感知到的东西——今天到底有没有在出活不能定位根因。业务掉了你还得往下钻到健康层、存活层
只盯存活层等于没监控。最常见的自欺是:面板上三个进程全绿,就写「系统稳定」。判断标准很简单——把看板给一个没用过这个系统的人看,他能不能说出「今天有没有人在用」。说不出,说明缺的是业务层。

三层的正确关系

业务层掉单 → 顺着健康层查是哪个接口超时 → 再顺着存活层查是内存爆了还是进程在重启环。三层是自上而下钻的路径,不是三个独立的图。做监控规划时,从业务层往下倒推,比从 CPU 往上堆要靠谱得多。

出片类系统特有的业务层指标:队列积压量。生成任务是有耗时的(生图、生视频、合成),一有波动就会堆积。积压量比「成功率」更早暴露问题——成功但积压 500 个队列,用户一样骂。
05

告警怎么设才有用

告警的唯一价值是让人去做一件事。不产生动作的告警,只是半夜的噪声。

类型触发逻辑例子适合什么
阈值型指标越过固定线内存 > 80%、磁盘 > 85%、P95 > 3 秒、1 小时重启 > 3 次资源类指标,波动小的
变化率型相对自身历史基线突变单据量比上周同时段跌 60%、错误率从 0.1% 跳到 8%业务量指标,绝对值没意义时
心跳缺失型该来的信号没来健康检查连续 3 次失败、每5 分钟应上报一次的指标没上报、日报没生成最难被绕过的一类——业务断了但机器没死,只有心跳能发现
心跳缺失型是最值得优先做的一类。因为它的失败模式是「静默」:进程还在、CPU 还跑着、接口甚至还返回 200,但实际业务早断了。存活层和健康层都可能骗你,只有「该来的没来」骗不了你。

一条好告警必须写清三件事

《告警定义模板》
  1. 触发条件(可量化,带时间窗):例:1 小时内重启次数≥3
  2. 严重级别(决定要不要叫醒人):P0叫醒 / P1工作时间处理 / P2日报汇总
  3. 收到后做什么(最重要):例:看 pm2 logs --lines 200 找堆栈,10 分钟内定位不到则升级
  4. 谁负责、怎么闭环:例:当班人,未处理则自动升级
红线:告警必须有人看,也必须有明确动作。没有指定责任人的告警 =没人看;没有写清动作的告警 = 看了也不知道干什么。两者都等于没设。

更要命的是告警疲劳:一次故障触发几百条告警,第一条之后就被全部静音了,等于没报。收敛优先级永远高于覆盖优先级——宁可少报一条,不要让关键告警被淹没。
06

成本护栏也是稳定性的一部分

这一节容易被当成「财务问题」,但对出片类系统来说,成本失控和宕机是同一种故障——都会让项目停摆。

LumenX 依赖 6 个以上付费外部 API:

DashScopeARK(火山方舟)KlingViduModelVerseOpenAI火山 TTSOSS 存储

这类调用的特点是:出片即烧钱,且失败重试也会烧钱。而原先这层是没有可见成本护栏的——意思是没人能在事发前知道今天花了多少,也没人能拦住「一个死循环任务连续调一万次视频接口」。

外部依赖成本管控表

护栏怎么做解决什么问题
日调用上限按接口维度设日配额,超限直接拒止住异常情况下的无限烧钱
超额熔断达到阈值后切到降级通道并告警,不继续重试重试风暴是成本爆炸的头号原因
缓存复用相同参数的结果缓存,重复请求不重复计费重试优先命中缓存而不是重打一次
降级到便宜模型预算或积压超限时,自动切低成本通道出草稿保住「能出活」这个底线,而不是直接瘫
用量日报按接口/按日/按业务方出用量与成本表没有可见性就没有护栏——看不见的花费一定失控
最贵的故障模式是「重试把成本放大一个数量级」。一个本来该失败的任务,如果超时设得不当再重试 5 次,配上视频模型的单次单价,一晚上能烧掉一个月预算。所以熔断和重试上限必须写进代码,不能靠人记得。
我的结论:稳定性不只是「不崩」。不崩但成本失控的系统,客户照样会叫停。所以交付清单里,护栏和监控是一起交的,不是等出事再补。
07

回滚与止损

不追求「永不出事」,追求出事能在几分钟内止血。止损和定位是两件事:先止住,再慢慢查。

什么情况止损动作事后复盘
新版本上线后错误率飙升立刻回滚到上一版,不带犹豫回滚后保留故障现场(不在生产机上查),离线复现
成本异常飙升 / 调用量异常先熔断上游接口,再查任务队列核对是重试放大、任务死循环,还是配置被改
磁盘写满,数据库写入失败清产物与历史日志,必要时临时扩盘定产物/日志保留周期,加水位告警
内存持续上涨打满重启该进程止血,重启前先导出日志查是否有未释放的大对象/ 未关闭的连接池
进程崩溃重启环(如 6 分钟一次)先摘流量/停调度,再排查根因,不要靠无限重启续命按第 02 节四件套定位,根因不清不许恢复调度
数据写错(脏数据入库)停写入,回滚数据/切只读,数据回退是单向门,必须有备份补校验规则和前置检查,不靠人工肉眼兜底
止损纪律:先恢复业务,再查根因。生产事故里最贵的错误是为了「查清楚再修」而让系统继续挂着。回滚是廉价操作,修复是昂贵操作——先做廉价的那个。
唯一的例外是涉及数据丢失或数据错乱的故障,那必须先停写再处理,否则止损动作本身会扩大损失。
08

常见坑 / 红线

坑为什么危险正确做法
只重启不定位重启销毁现场,根因永远查不出来,同类故障反复发生重启前先 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 各自看什么
  • ✓我知道内存 > 80%、磁盘 > 85%、1 小时重启 > 3 次就该报警
  • ✓我能用 ps -eo pid,ppid,cmd | awk '$2==1' 找出孤儿进程
  • ✓我理解 address already in use 可能是孤儿进程占端口,而不是代码 bug
  • ✓我知道排查顺序是:先看数→ 看日志 → 看内核,全程只读
  • ✓我分得清存活层 / 健康层 / 业务层,并能说出每层证明不了什么
  • ✓我知道为什么心跳缺失型告警最不能省
  • ✓我给外部 API 配了日配额、熔断、缓存、降级、用量日报
  • ✓我知道什么时候该立刻回滚,什么时候必须先停写再处理
  • ✓我的体检过程不改生产配置,只读取证
面试怎么答

「你怎么保证系统稳定?你遇到过最严重的故障是什么?」

我先讲一个我自己查出来的真实问题,因为我觉得它比讲成功案例更能说明我懂生产环境。

我给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 存储),出片即烧钱,而原先没有可见的护栏。我会配日调用上限、超额熔断、缓存复用、降级到便宜模型和用量日报——重试是最贵的故障模式,一个本该失败的任务重试几次,配上视频模型单价,一晚上能烧掉一个月预算。不崩但成本失控,系统照样会被叫停。