📑 本页目录(点开跳转)
05 · 该监控什么
⏱ 40 分钟 | ⭐ 三层监控体系
🎯 一句话
大部分团队只监控了服务是否存活,而模型的问题几乎从不表现为服务挂掉。 你需要三层监控:系统层、数据层、模型层 —— 而后两层才是这一章的重点。
🏛️ 一、三层监控
- 「它给出的答案还合理吗」
- 「喂进去的东西还对吗」
- 「服务还活着吗」
🔑 一个反直觉的事实: 模型出问题时,系统层几乎总是正常的。 特征算错了、上游埋点变了、分布漂移了 —— 服务照样 200 OK、延迟照样 20ms。 只监控第①层,等于没监控模型。
① 系统层(简单,但有两个模型特有的点)
| 指标 | 阈值参考 |
|---|---|
| QPS、错误率、P50/P99 延迟 | 按 SLA 定 |
| CPU / 内存 / GPU 利用率 | — |
| 模型加载耗时 / 版本号 ⭐ | 部署后确认版本真的变了 |
| 降级触发次数 ⭐⭐ | >0 就该看 |
⚠️ 降级次数是最容易漏的一个指标(第 3 章提过): 系统降级时不报错,它「正常工作」着。不监控它,可能几个月都不知道模型根本没在跑。
② 数据层:性价比最高的一层 ⭐⭐
对每个进入模型的特征,监控四件事:
1. 缺失率 —— 突然从 2% 涨到 40%?上游挂了
2. 取值范围 —— 出现了训练时没见过的极值?单位可能变了
3. 分布 —— 均值/分位数漂移(第 6 章详讲)
4. 类别集合 —— 出现新类别?消失了某个类别?⭐
🔨 一个够用的实现
import numpy as np, pandas as pd, json
def feature_profile(df: pd.DataFrame) -> dict:
"""训练时算一次存下来,线上定期算同样的指标做对比"""
prof = {}
for col in df.columns:
s = df[col]
d = {"missing_rate": float(s.isna().mean())}
if np.issubdtype(s.dtype, np.number):
q = s.quantile([.01, .25, .5, .75, .99]).to_dict()
d.update(kind="num", mean=float(s.mean()), std=float(s.std()),
**{f"p{int(k*100)}": float(v) for k, v in q.items()})
else:
vc = s.value_counts(normalize=True)
d.update(kind="cat", n_unique=int(s.nunique()),
top=vc.head(20).to_dict()) # ⭐ 存 top-K 占比
prof[col] = d
return prof
def check(base: dict, live: dict, missing_jump=0.1, mean_sigma=3):
"""返回需要告警的特征。规则简单但在实践中够用。"""
alerts = []
for col, b in base.items():
l = live.get(col)
if l is None:
alerts.append((col, "特征消失了")); continue
if l["missing_rate"] - b["missing_rate"] > missing_jump:
alerts.append((col, f"缺失率 {b['missing_rate']:.1%}→{l['missing_rate']:.1%}"))
if b["kind"] == "num" and b["std"] > 0:
z = abs(l["mean"] - b["mean"]) / b["std"] # ⭐ 用训练集 std 归一
if z > mean_sigma:
alerts.append((col, f"均值漂移 {z:.1f}σ"))
if b["kind"] == "cat":
new = set(l["top"]) - set(b["top"])
if new:
alerts.append((col, f"新类别 {list(new)[:3]}"))
return alerts
⭐ 关键设计:用训练集的 std 做归一化,而不是线上自己的 std。 否则线上分布整体平移时,用它自己的 std 算 z 分数会看不出异常。
⚠️ 数据层监控的三个坑
| 坑 | 说明 |
|---|---|
| 只监控入模特征,不监控上游原始字段 | 特征是算出来的,原始字段变了但特征看起来正常是可能的 |
| 按天聚合,掩盖了小时级异常 ⭐ | 上游挂了 3 小时,按天看均值只动了一点 |
| 告警阈值一刀切 | 有的特征天然波动大(如「距上次访问时长」),要分别定阈值 |
③ 模型层:它给的答案还合理吗
1. 预测值分布 —— 均值、方差、分位数
⭐ 突变几乎总是坏消息
2. 预测的置信度 —— 高置信样本占比下降 = 模型开始「不确定」
3. 输出多样性 —— 推荐/生成场景:是不是塌缩到少数几个答案
4. 业务指标 —— CTR、转化率、人均时长
5. 有标签后的真实指标 —— AUC、准确率(但会延迟,见第 7 章)
💡 预测分布突变的三种典型形态
① 整体平移 → 多半是某个特征的单位/口径变了
② 方差塌缩 → 模型开始给所有人差不多的分数
常见于特征大面积缺失(退化成只用偏置)⭐
③ 双峰变单峰 → 某一类样本消失了,或被错误地归到一起
⭐ 第 ② 种特别值得记:特征大面积缺失时,模型不会报错, 它会退化成「只用还剩下的那些特征 + 偏置」预测 —— 表现就是所有人的分数都挤在一起。 监控预测值的标准差,能第一时间发现它。
🎯 二、怎么定阈值(不用统计学也能做)
❌ 拍脑袋定「均值不能变超过 10%」
✅ 用历史数据反推:
① 取过去 30 天的每日指标
② 算它自己的均值和标准差
③ 阈值 = 均值 ± 3σ(或用分位数:超出历史 P1~P99)
④ 用剩下的历史数据回测:这个阈值会报多少次警?
⭐ 第 ④ 步最关键 —— 如果回测显示一个月要报 50 次警,
这个阈值一定会被无视(第 8 章)
📋 三、一份最小可用的监控清单
如果时间只够做 8 个指标,做这 8 个:
| # | 指标 | 层 | 为什么是它 |
|---|---|---|---|
| 1 | 错误率 / P99 延迟 | 系统 | 基本盘 |
| 2 | 降级触发次数 | 系统 | 静默失效的唯一信号 ⭐ |
| 3 | 模型版本号 | 系统 | 确认部署真的生效了 |
| 4 | 每个特征的缺失率 | 数据 | 上游故障最快的信号 ⭐⭐ |
| 5 | Top-5 重要特征的均值漂移 | 数据 | 抓大放小 |
| 6 | 预测值的均值和标准差 | 模型 | 标准差塌缩=特征大面积缺失 ⭐ |
| 7 | 核心业务指标(按小时) | 模型 | 最终目标 |
| 8 | 训练/线上特征一致性抽检 | 数据 | 第 4 章那个问题 ⭐⭐ |
💡 注意第 5 条「Top-5 重要特征」: 不用监控全部 200 个特征 —— 按重要性排序,盯住最重要的那几个, 覆盖了绝大部分风险,告警量还可控。 🔗 特征重要性怎么算、什么时候会骗你 → 第 9 章
🕰️ 四、被漏掉的第四类:新鲜度
前面三层都在问"值对不对"。还有一个问题几乎没人监控:这个值是什么时候算的。
特征分布 ✅ 正常
缺失率 ✅ 正常
取值范围 ✅ 正常
但它是【三天前】算出来的 💀
→ 所有分布检查都会通过,因为三天前的分布本来就是对的
要监控的三个新鲜度:
| 新鲜度 | 定义 | 典型阈值 |
|---|---|---|
| 特征新鲜度 | 特征值的计算时间距现在多久 | 实时特征 < 5 分钟;T+1 特征 < 30 小时 |
| 上游表新鲜度 ⭐ | 依赖的每张表最后一个分区的时间 | 按调度周期 + 1.5 倍冗余 |
| 模型新鲜度 | 当前线上模型的训练截止时间 | 按重训周期定(第 16 章) |
# ⭐ 一个直接可跑的新鲜度检查:只查元数据,不扫全表,几乎零成本
import datetime
FRESHNESS_SLA = { # 表名 -> 允许的最大滞后(小时)
"dw_user_action_di": 30, # T+1 日表,给 6 小时冗余
"rt_user_profile": 0.25, # 实时表,15 分钟
"dim_item_info": 72, # 维表,3 天
}
def check_freshness(get_latest_partition_ts):
now, alerts = datetime.datetime.now(), []
for tbl, sla_h in FRESHNESS_SLA.items():
ts = get_latest_partition_ts(tbl) # ⭐ 只读元数据
lag = (now - ts).total_seconds() / 3600
if lag > sla_h:
# ⭐ 直接给出「已经用旧数据服务了多久」,而不是只说"表旧了"
alerts.append(f"{tbl} 滞后 {lag:.1f}h(SLA {sla_h}h),超出 {lag - sla_h:.1f}h")
return alerts
⭐ 为什么这一项性价比极高: 它是唯一能在"坏数据进入模型之前"就报警的检查 —— 分布监控要等坏数据流进来才看得见,新鲜度检查在上游没跑完的那一刻就知道。 🔗 第 1 章事故 B(假期调度没跑、被当成"假期效应")—— 一条新鲜度告警就能挡掉那两周的浪费。
📉 五、聚合粒度:一个能让监控完全失效的选择
💀 真实事故:所有监控都在,但什么都没报
某推荐服务的用户画像表,凌晨 2:00–5:00 因上游故障返回空
→ 这 3 小时里,用户侧特征全部走默认值
→ 这 3 小时的 CTR 从 4.2% 掉到 1.1%(−74%)
监控为什么没报:
· 特征缺失率是【按天】算的
全天缺失率 = 3/24 × 100% + 21/24 × 2% ≈ 14.3%
告警阈值是「缺失率涨幅 > 10 个百分点」
→ 14.3% − 2% = 12.3pp → 刚好触发了?
⚠️ 并没有:告警是【第二天早上 8 点】跑批时才算出来的
→ 距离故障开始已经 30 小时,距离结束 27 小时
→ 值班同学看到时,问题早就自己好了 → 标成"已恢复,不处理"
| 粒度 | 那 3 小时的 CTR 在图上长什么样 | 会不会被发现 |
|---|---|---|
| 按天 | 4.2% → 3.8%(−9%) | ❌ 在正常波动内 |
| 按小时 | 4.2% → 1.1%(−74%) | ✅ 一眼就看见 ⭐ |
| 按 5 分钟 | 同上,但噪声更大 | ✅ 但误报会变多 |
⭐ 一条实用的经验法则: 监控的时间粒度,要比你能容忍的故障时长更细。 能容忍 1 小时故障 → 至少按 10 分钟聚合;能容忍 1 天 → 按小时。 按天聚合的监控,只能发现持续好几天的问题。
⚠️ 数据层监控的另外三个坑(补充第②节那三个):
| 坑 | 后果 | 怎么修 |
|---|---|---|
| 只存聚合值,不存直方图 | 均值没变但形状变了(双峰→单峰),完全看不出 | 存分位数或固定分桶计数 ⭐ |
| 基线是"上周同期"而不是训练集 | 慢漂移会被一路跟随,永远不报警 💀 | 固定基线用训练集,同比只用来做告警去周期 |
| 监控用的是采样后的日志 | 采样率变了,指标跟着变,误以为是漂移 | 记录采样率,指标要除以它 |
⭐ 第二条最隐蔽:如果基线永远是"上周",那么每周漂移 2% 的特征永远不会告警, 而一年后它已经漂了 65%。 正确做法:同时看两个基线 —— 训练集(判断"离训练分布多远")和上周(判断"最近有没有突变")。
💾 监控本身的成本(一个常被忽略的账)
| 做法 | 存储量级(200 特征 / 日均 1 亿请求) | 备注 |
|---|---|---|
| 全量落特征日志 | ~TB / 天 | 最全,但很多团队负担不起 |
| 按小时聚合 + 分位数 ⭐ | ~MB / 天 | 性价比最高,够用 |
| 抽样 1% 落原始特征 | ~10 GB / 天 | 用于逐字段比对和排查 ⭐ |
💡 推荐组合:聚合指标全量算(便宜)+ 原始特征抽样 1% 落盘(用于排查)。 前者负责"发现异常",后者负责"查清原因" —— 两件事的数据需求完全不同。
🔗 六、和站内其他章的关系
| 相关的地方 | 和这一章的关系 |
|---|---|
| 第 3 章降级监控 | 系统层第 4 项 |
| 第 4 章一致性 | 清单第 8 项 |
| 全景导论 13 LLMOps | LLM 场景的监控 |
| 推荐算法 15 | 推荐场景的监控项 |
✅ 检查点
- 三层监控分别是什么?为什么说只做第一层等于没监控模型?
- 系统层最容易漏的模型特有指标是哪个?为什么?
- 数据层监控哪四件事?
- 为什么归一化要用训练集的 std 而不是线上的?
- 数据层监控的三个坑是什么?
- 预测值方差塌缩通常意味着什么?
- 定阈值时最关键的是哪一步?
- 为什么只监控 Top-5 重要特征就够?
- 「新鲜度」为什么是分布监控查不出来的?要监控哪三个新鲜度?为什么说它性价比极高?
- 那个 3 小时故障为什么所有监控都没报出来?按天和按小时看差别有多大?
- 「监控的时间粒度」该怎么定?
- 为什么基线不能只用"上周同期"?正确做法是什么?
- 监控数据该怎么存才划算?
👀 答案
- 系统层(还活着吗)、数据层(喂进去的还对吗)、模型层(答案还合理吗)。只做第一层等于没监控模型,因为模型出问题时系统层几乎总是正常的——特征算错、埋点变了、分布漂移,服务照样 200 OK、延迟正常。
- 降级触发次数。因为降级时不报错、"正常工作"着,不监控它可能几个月都不知道模型根本没在跑。
- 缺失率、取值范围、分布、类别集合(新类别/消失的类别)。
- 因为线上分布整体平移时,用它自己的 std 算 z 分数会看不出异常——基准必须是固定的训练集。
- ①只监控入模特征不监控上游原始字段 ②按天聚合掩盖小时级异常(上游挂 3 小时,按天看均值只动一点)③告警阈值一刀切。
- 特征大面积缺失。模型不报错,会退化成"只用剩下的特征+偏置"预测,表现就是所有人分数挤在一起。监控预测值标准差能第一时间发现。
- 用剩下的历史数据回测这个阈值会报多少次警。如果一个月报 50 次,它一定会被无视。
- 因为按重要性排序盯住最重要的几个,能覆盖绝大部分风险,同时告警量可控——不用监控全部 200 个特征。
- 因为三天前算出来的值,它的分布本来就是对的——所有分布检查都会通过。三个新鲜度:特征新鲜度(值的计算时间距现在多久)、上游表新鲜度(每张依赖表最后一个分区的时间)、模型新鲜度(训练截止时间)。性价比高是因为它是唯一能在坏数据进入模型之前就报警的检查——分布监控要等坏数据流进来才看得见。
- 因为缺失率是按天算的、而且第二天早上 8 点才跑批:全天缺失率被稀释成 14.3%,且告警产出时距故障开始已 30 小时、结束 27 小时,值班同学看到时问题已自己好了。按天看 CTR 只从 4.2% 掉到 3.8%(−9%,在正常波动内),按小时看是 4.2%→1.1%(−74%),一眼就看见。
- 监控的时间粒度要比你能容忍的故障时长更细:能容忍 1 小时故障就至少按 10 分钟聚合,能容忍 1 天就按小时。按天聚合的监控只能发现持续好几天的问题。
- 因为慢漂移会被一路跟随,永远不报警——每周漂 2% 的特征一年后已经漂了 65%,但每周环比都正常。正确做法:同时看两个基线——训练集(判断"离训练分布多远")和上周(判断"最近有没有突变")。
- 聚合指标全量算(按小时 + 分位数,MB 量级)+ 原始特征抽样 1% 落盘(用于排查)。前者负责"发现异常",后者负责"查清原因",两件事的数据需求完全不同。全量落特征日志是 TB/天,多数团队负担不起。
🛑 可以停在这里
⚡ 走神救援
⭐三层监控:系统层(还活着吗)/ 数据层(喂进去的还对吗)/ 模型层(答案还合理吗)。⭐⭐反直觉的关键事实:模型出问题时系统层几乎总是正常的——特征算错、埋点变了、分布漂移,服务照样200 OK延迟20ms,只监控系统层等于没监控模型。系统层最容易漏的是降级触发次数(降级不报错、"正常工作"着)。数据层性价比最高,监控四件事:缺失率、取值范围、分布、类别集合;⭐归一化要用训练集的std(否则线上整体平移时看不出异常)。三个坑:只监控入模特征不看上游原始字段、按天聚合掩盖小时级异常、阈值一刀切。模型层看预测分布,⭐方差塌缩=特征大面积缺失(模型退化成只用偏置,所有人分数挤在一起)。⭐定阈值最关键的一步是用历史数据回测会报多少次警——一个月50次的阈值一定被无视。最小8个指标里最该做的:降级次数、每特征缺失率、Top-5特征均值漂移、预测值均值和标准差、训练/线上一致性抽检。⭐被漏掉的第四类:新鲜度——分布全对但值是三天前算的,所有分布检查都会通过;要看特征新鲜度 / 上游表新鲜度 / 模型新鲜度,它是唯一能在坏数据进入模型之前就报警的检查。💀聚合粒度能让监控完全失效:3 小时上游故障,按天看 CTR 只掉 9%(正常波动内),按小时看是 −74%;而且按天的告警第二天 8 点才算出来,看到时问题已自己好了。⭐法则:监控粒度要比你能容忍的故障时长更细(能容忍 1 小时就按 10 分钟聚合)。三个补充坑:只存聚合值不存直方图(双峰变单峰看不出)、💀基线只用"上周同期"(每周漂 2% 永远不告警,一年漂 65% → 必须同时看训练集基线和上周基线)、监控用采样日志但没记采样率。成本账:全量特征日志 TB/天,⭐按小时聚合+分位数只要 MB/天——聚合指标全量算负责发现,原始特征抽样 1% 落盘负责查因。
下一节 👉 06-数据漂移与概念漂移.md