🏠 总目录📚 本教程 11 · 从告警到根因 ← →
📑 本页目录(点开跳转)

11 · 从告警到根因

⏱ 42 分钟 | ⭐ 一份可以照抄的排查手册


🎯 一句话

线上排查最贵的不是修,是「不知道从哪查」。 这一章给一条固定的排查顺序 —— 从最便宜、最可能的原因开始,逐级往下。


🪜 一、排查顺序:便宜的先查

操作步骤

  1. 是真的吗? —— 先确认指标本身没坏 5 分钟
  2. 是什么时候开始的? —— 定位时间点,圈定嫌疑范围 10 分钟 ⭐
  3. 是谁变了? —— 我方发布 / 上游 / 外部 15 分钟
  4. 是哪部分流量? —— 分人群、分渠道拆开 15 分钟 ⭐
  5. 是哪个特征? —— 特征分布 + SHAP 对比 30 分钟
  6. 是模型本身吗? —— 最后才怀疑模型 —

🔑 ⑥ 放在最后不是随意的: 真正「模型自己变差了」的情况远比想象的少。 绝大多数线上问题是数据、管道、配置、外部变化造成的。 一上来就重训模型,是最常见的浪费。


① 是真的吗(别急着查)

逐项核对

  • 指标口径最近改过吗?(埋点、SQL、看板定义)

  • 数据是不是还没跑完?(离线任务延迟会让当天指标偏低)⭐

  • 对比的基线选对了吗?(昨天是节假日吗)

  • 分母变了吗?(比率类指标:分母涨了也会让比率跌)⭐

⭐ 「分母变了」这条每年都会坑到人: CTR 跌了不一定是点击少了,可能是曝光暴涨(比如上了个新坑位)。 看比率类指标时,永远同时看分子和分母的绝对值。


② 什么时候开始的 ⭐ —— 最有价值的一步

结果对照

把指标按【小时】画出来,找到拐点
⭐ 拐点的形状直接告诉你原因类型:
突变(一小时内断崖)
发布、配置变更、上游故障
去查那个时间点前后一小时的所有变更 ⭐
阶梯(某天开始换了个平台)
上游数据源变了、口径变了
缓慢下滑(几周)
漂移(第 6 章),不是故障
周期性(每天同一时间)
定时任务、批处理窗口、跨时区流量

⭐ 拐点时间是排查里信息量最大的一个数字。 有了它,你能把「所有可能原因」缩小到「那个时间点附近发生的事」—— 通常就剩三五件事可查。

配套要有的东西:一条变更时间线

对照

把这些放进同一张图(或同一个查询):

· 模型/服务发布记录

· 配置变更

· 上游数据表的产出时间和行数

· 运营活动排期

· 外部事件(大促、节假日)

⭐ 这个「变更时间线」值得专门做一个看板,

它能把 ③ 那一步从半小时压缩到一分钟

拐点其实可以让机器帮你找(不用肉眼盯曲线):

import numpy as np

def find_changepoint(series, min_seg=6):
    """最小二乘单拐点检测:把序列切成两段,找「组内方差和」最小的切点。
    series: 按小时(或按天)排好序的指标序列"""
    x = np.asarray(series, dtype=float)
    n = len(x)
    best, best_cost = None, np.inf
    for k in range(min_seg, n - min_seg):
        cost = x[:k].var() * k + x[k:].var() * (n - k)   # ⭐ 两段各自越"平"越好
        if cost < best_cost:
            best, best_cost = k, cost
    a, b = x[:best], x[best:]
    jump = b.mean() - a.mean()
    se = np.sqrt(a.var() / len(a) + b.var() / len(b)) + 1e-12
    # ⭐ 关键是这个 z:跳变幅度要和"本来的波动"比,否则天天都能找出拐点
    return {"拐点位置": best, "跳变幅度": jump, "显著性z": jump / se}

# 用法:把最近 14 天 × 24 小时的指标喂进去,拿到「第几个小时」

⭐ 一定要看那个 z,不要只看 best: 这个函数永远会返回一个拐点,哪怕序列完全是白噪声。 经验阈值:|z| < 3 时基本可以认为「没有拐点,只是在正常波动」 —— 这时候你要查的不是「谁变了」,而是回到 ① 确认「跌」是不是真的存在。


③ 谁变了

嫌疑 怎么查
我方发布 发布记录对时间点;先回滚验证(第 3 章)⭐
上游数据 表产出时间、行数、schema 变更记录
配置 阈值、截断长度、降级开关、AB 分流比例
外部 大促、竞品、节假日、舆情

⭐ 一个关键原则:能回滚就先回滚,再慢慢查。 止损优先于定位。 回滚后指标恢复了,也顺便验证了「就是这次发布的问题」。


④ 哪部分流量

信息关系

把指标按这几个维度拆开,看跌的是全体还是某一撮:
· 新用户 / 老用户
· 渠道(自然量 / 投放 / 各分发位)
· 设备与版本(某个 App 版本?某个机型?)
· 地域
· 实验分组 ⭐
⭐ 如果只有某一撮跌→范围立刻缩小 90%
⭐ 如果全体均匀跌→多半是模型或全局配置

💡 拆开看还能发现第 14 章那种情况: 总体没变,但两个人群一涨一跌互相抵消 —— 那其实是两个问题。


⑤ 哪个特征(这一步用第 5、9、10 章的工具)

结果对照

① 看特征监控看板:哪些特征的分布在拐点处变了(第 5、6 章)
② 按重要性排序,只看 Top-10(第 9 章)
③ 取一批「异常时段」样本和一批「正常时段」样本
分别算 SHAP,对比每个特征的平均贡献 ⭐(第 10 章)
贡献变化最大的那个,通常就是元凶
import numpy as np, pandas as pd, shap

def diff_shap(model, X_bad, X_good, top=10):
    """对比异常时段和正常时段,找出贡献变化最大的特征"""
    ex = shap.TreeExplainer(model)
    sb = ex.shap_values(X_bad).mean(axis=0)     # ⭐ 带符号取均值,看方向
    sg = ex.shap_values(X_good).mean(axis=0)
    d = pd.DataFrame({
        "特征": X_bad.columns,
        "异常时段贡献": sb, "正常时段贡献": sg, "变化": sb - sg,
        "特征均值变化": X_bad.mean().values - X_good.mean().values,
    })
    return d.reindex(d["变化"].abs().sort_values(ascending=False).index).head(top)

⭐ 同时看「贡献变化」和「特征均值变化」这两列,能直接分辨两种情况:

贡献变了 特征均值变了 说明
✅ ✅ 输入变了(数据/管道问题)⭐ 最常见
✅ ❌ 输入没变但作用变了 → 可能是模型或编码问题

⑥ 最后才是模型本身

对照

走到这一步还没找到,才考虑:

· 概念漂移(第 6 章)—— 需要有标签才能确认

· 模型文件损坏 / 加载了错误版本(先看版本号!)

· 训练/推理不一致(第 4 章)—— 跑一次逐字段比对

⚠️ 「加载了错误版本」比想象中常见,而且查起来只要 10 秒 —— 建议把它提到第 ① 步一起查。


💀 二、一次真实的排查复盘(完整时间线)

这个案例值得逐行读,因为它踩中了本教程前十章里的三个坑, 而且它的特征是「所有技术指标都正常」。

对照

系统:反欺诈拦截模型,线上约 4000 QPS

特征来自三处:

· 请求参数 (实时,随请求带来)

· 用户画像 (Redis)

· 近 7 天行为聚合 (离线表,每天 04:00 产出)

告警:周二 09:12,拦截率从 1.8% 掉到 1.1%(告警阈值 1.5%)。

时刻 动作 看到了什么
09:15 ① 是真的吗 拦截数 7.2 万/天 → 4.4 万,请求量没变 → 不是分母问题;模型版本号 v213,与上周一致 → 不是发版
09:20 ② 什么时候开始 按小时画:周一 02:00 一小时内断崖 → 突变型 → 只查那一小时前后
09:25 ③ 谁变了 变更时间线看板上,02:00 没有我方发布;但 01:50 用户画像 Redis 集群做过一次扩容
09:40 顺着 Redis 查 扩容后新节点的 key 前缀配置写错一位 → 读不到 → 特征服务读超时 → 触发降级,返回默认值
09:50 ④ 哪部分流量 按「是否走了降级链路」拆开:降级组拦截率 0.3%,正常组 1.85% → 范围锁死
10:05 止损 回滚 Redis 配置;10:20 指标恢复

💀 这个故障最恶心的地方: 降级是设计好的行为,所以整条链路"完全健康" —— 接口成功率 100%,错误率 0,P99 延迟反而从 42ms 降到 26ms (因为不查 Redis 了,更快了)。 一个只看「服务健康」的监控体系,对它是全盲的。

代价:

对照

从周一 02:00 到周二 10:20 = 32 小时

期间约 41% 的请求走了降级链路

按事后回捞:漏放的欺诈交易约 4200 笔

⭐ 发现延迟 = 31 小时

—— 而拦截率跌破阈值到有人处理,只花了 8 分钟

真正的损失全在「发现」这一段

为什么没被早发现(三条,每条都对应前面某一章):

缺失 对应章节 补上之后
特征缺失率 / 默认值占比没进监控 第 5 章输入层 默认值占比 > 2% 就告警
降级链路没打点 第 17 章「走了哪条链路」 降级比例 > 5% 直接 P1
告警只挂在业务指标上 第 5 章三层监控 业务层是最慢的一层,输入层能提前十几小时

⭐ 改进之后,三个月后同类问题(另一个特征源限流)再次发生: 发现延迟从 31 小时降到 4 分钟。 这就是第 8 章说的「每次事故都该让发现延迟下降一点」的实际样子。


🩺 三、症状 → 最先怀疑什么(速查)

这张表是上面那套流程的「快捷方式」:先看症状对号入座,猜错了再回到六步流程。

症状 最先怀疑 一分钟内怎么验证
指标断崖 + 成功率 100% + 延迟反而下降 ⭐ 降级链路被触发 看降级/兜底比例的打点
指标缓慢下滑数周 漂移,不是故障 看 PSI 曲线(第 6 章)
只有某个 App 版本 / 机型跌 客户端埋点或协议变更 按版本号拆开
预测分数分布整体平移 ⭐ 某个特征量纲变了 看该特征的分位数,不是均值
新模型刚上线就差、离线却很好 训练/推理不一致 逐字段比对(第 4 章)
每天固定时间跌一段 上游批任务延迟 看上游表的产出时间
总体跌,但每个人群都涨 💀 流量结构变了(辛普森) 看各组的样本量占比(第 14 章)
恰好一半流量出问题 灰度批次 / 某个机房 / 某组实例 按实例 IP、机房拆
指标正常但方差突然变大 某个上游间歇性超时 看该依赖的 P99 和超时率

⭐ 第一行是最值得记住的一条: 「所有技术指标都正常,业务指标却掉了」几乎总是降级、兜底或默认值。 因为真正的故障会留下错误率,而降级不会。


📋 四、一份可以直接用的排查模板

## 事件:<指标> 于 <时间> 下降 <幅度>

### ① 确认
- [ ] 口径未变 / 数据已跑完 / 基线合理 / 分母正常
- [ ] 模型版本号符合预期

### ② 时间点
- 拐点:______(精确到小时)
- 形状:突变 / 阶梯 / 缓降 / 周期

### ③ 该时间点附近的变更
| 时间 | 变更 | 相关性 |
|---|---|---|

### ④ 影响范围
| 维度 | 受影响的分组 | 未受影响的分组 |
|---|---|---|

### ⑤ 特征层面
| 特征 | 贡献变化 | 均值变化 |
|---|---|---|

### 结论
- 根因:
- 止损动作 / 时间:
- **发现延迟**:______(从发生到发现)⭐
- 改进项:新增什么监控能提前 N 小时发现

⭐ 最后两行是复盘的价值所在(第 8 章): 每次事故都应该让「发现延迟」下降一点。


🧠 五、六个经验法则

操作步骤

  1. 止损优先于定位 —— 能回滚就先回滚 ⭐
  2. 一次只改一个变量 —— 否则恢复了也不知道是哪个起作用
  3. 相信数据,不相信记忆 —— 「我记得没改过」经常是错的
  4. 排查过程要边做边记 —— 否则第二天复盘时全忘了
  5. 先缩小范围,再深入细节 —— 二分优于遍历 ⭐
  6. 「没报错」不等于「没出错」 —— 降级、兜底、默认值都是静默的 ⭐

💡 第 ② 条在紧急情况下最容易违反: 慌了就同时回滚模型、改配置、重启服务 —— 恢复了,但你永远不知道原因,下次还会再来一遍。

⭐ 第 ⑤ 条的具体做法:排查像二分查找 —— 每一步都问「这个问题在哪一半」,而不是「是不是这个原因」。 六步流程本身就是一条二分链:时间维度二分 → 流量维度二分 → 特征维度二分。 一个维度平均能砍掉一半可能性,三个维度就是 1/8。


⚠️ 六、排查时最常踩的七个坑

坑 后果 怎么修
一上来就重训模型 ⭐ 花两天训完,问题还在(根因在上游) 严格按六步走,模型永远放最后
同时改三件事 恢复了但不知道谁起的作用,下周原样再来 一次只改一个,每个改动写进时间线
只看总体曲线 「只有 iOS 某版本跌」这类问题永远看不到 告警自带按 4–5 个维度的自动下钻
拐点按「天」看 ⭐ 突变被看成缓降 → 误判成漂移 → 误决策去重训 排查时一律按小时画
用「本周 vs 上周」当唯一基线 撞上节假日全是假告警,撞上大促全是漏报 同比 + 环比两条基线一起看
相信「我记得没改过」 90% 的「没改过」经不起查 只认变更记录,不认记忆
查完不写记录 三个月后同一个问题从头再查一遍 用下面的模板,「发现延迟」必须写

🔗 七、和站内其他章的关系

相关的地方 和这一章的关系
第 5 章三层监控 ⑤ 的数据来源
第 6 章漂移 ⑥ 的判断
第 9 章重要性 ⑤ 的排序依据
第 10 章 SHAP ⑤ 的对比工具
第 17 章链路打点 案例里最关键的那条日志
ML基础 11训练调试 那是训练时,这是线上
AI基础设施 23可观测性 基础设施层的同一套思路

✅ 检查点

  1. 排查的六个步骤是什么?为什么「模型本身」放最后?
  2. 「是真的吗」这一步要查哪四件事?哪一条每年都坑人?
  3. 为什么说拐点时间是信息量最大的数字?四种形状各指向什么?自动找拐点时为什么必须看 z 而不只看位置?
  4. 「变更时间线」该包含什么?它省了哪一步的时间?
  5. 排查时的第一原则是什么?
  6. 按流量维度拆开看,两种结果分别意味着什么?
  7. SHAP 对比时,同时看「贡献变化」和「特征均值变化」能分辨什么?
  8. 那个 Redis 扩容的案例里,为什么所有技术指标都正常?发现延迟是多少、后来降到了多少?「成功率 100%、延迟反而下降、但业务指标掉了」几乎总是什么原因?
  9. 紧急情况下最容易违反哪条经验法则?后果是什么?
👀 答案
  1. ①是真的吗 ②什么时候开始的 ③谁变了 ④哪部分流量 ⑤哪个特征 ⑥模型本身。放最后是因为真正"模型自己变差"的情况远比想象少,绝大多数是数据/管道/配置/外部变化——一上来就重训是最常见的浪费。
  2. 口径改过吗、数据跑完了吗、基线选对了吗、分母变了吗。「分母变了」每年坑人:CTR 跌可能是曝光暴涨(上了新坑位)不是点击少了。
  3. 因为有了它能把"所有可能原因"缩小到"那个时间点附近发生的事",通常只剩三五件可查。形状:突变→发布/配置/上游故障;阶梯→数据源或口径变了;缓降→漂移(不是故障);周期性→定时任务或跨时区。自动检测永远会返回一个拐点(哪怕输入是白噪声),所以必须看跳变幅度相对段内波动的 z 值;|z| 小于 3 时应当认为「只是正常波动」,这时要回到 ① 去确认「跌」是不是真的存在。
  4. 发布记录、配置变更、上游表产出时间和行数、运营排期、外部事件。它把第③步从半小时压缩到一分钟。
  5. 止损优先于定位——能回滚就先回滚,回滚后恢复也顺便验证了是这次发布的问题。
  6. 只有某一撮跌 → 范围缩小 90%;全体均匀跌 → 多半是模型或全局配置。
  7. 贡献变了 + 均值也变了 → 输入变了(数据/管道问题,最常见);贡献变了但均值没变 → 输入没变但作用变了,可能是模型或编码问题。
  8. 因为触发的是"设计好的降级":Redis 扩容后 key 前缀配错 → 特征读超时 → 返回默认值。接口成功率 100%、错误率 0、P99 延迟反而从 42ms 降到 26ms(不查 Redis 更快了)。发现延迟 31 小时(跌破阈值到有人处理只用了 8 分钟,损失全在"发现"这一段);补上「默认值占比」监控和降级链路打点后,同类问题的发现延迟降到 4 分钟。所以「技术指标全正常、业务指标却掉了」几乎总是降级、兜底或默认值被触发——真正的故障会留下错误率,而降级不会,它是被设计出来的正常行为。
  9. 一次只改一个变量。慌了会同时回滚模型、改配置、重启服务 —— 恢复了但永远不知道原因,下次还会再来。

🛑 可以停在这里

⚡ 走神救援

⭐ 排查顺序:便宜的先查。 ①是真的吗 ②什么时候开始的 ③谁变了 ④哪部分流量 ⑤哪个特征 ⑥模型本身。

⭐ ⑥ 放最后,因为「模型自己变差」远比想象中少,绝大多数是数据、管道、配置或外部变化——一上来就重训是最常见的浪费。

⭐ 拐点时间是信息量最大的数字,它能把可能原因缩到那个时间点附近的三五件事。形状直接指向类型:突变 → 发布/配置/上游;阶梯 → 数据源或口径变了;缓降 → 漂移不是故障;周期 → 定时任务。

原则:止损优先于定位,能回滚先回滚;一次只改一个变量(慌了同时回滚+改配置+重启,恢复了也永远不知道原因);先缩范围再看细节。

💀 那个 Redis 扩容配错前缀的案例:特征读超时触发了设计好的降级,于是成功率 100%、错误率 0、延迟反而更好,只有拦截率悄悄掉了——一个只看服务健康的监控体系对它完全失明。

⭐ 最该记住的速查:技术指标全正常、业务指标却掉了 → 几乎总是降级、兜底或默认值。 真故障会留下错误率,降级不会。所以「没报错」不等于「没出错」。

下一节 👉 12-AB实验你以为你懂了.md

打卡记录保存在你的浏览器里,首页能看到总进度