1.1 指标分四层,缺一层就无法定位
上一节:无 | 下一节:1.2 百分位不可平均 配套代码:01-metrics-and-slo/01-four-layers-of-metrics
一句话结论
性能指标分四层:业务层、延迟层、资源层、饱和度层。 只监控前两层,你会在「服务变慢了」和「为什么慢」之间卡住;而饱和度层是最早的预警信号,它在用户受影响之前就开始报警。
一、用汽车仪表盘理解「为什么指标要分层」
想象你开车,车上只有一个速度表:
- 速度表显示 60 km/h —— 车在正常行驶。
- 突然,速度掉到 20 km/h。
你知道出问题了,但完全不知道原因:是没油了?发动机过热?轮胎漏气?还是前面堵车?
现在把仪表盘补全:
| 仪表 | 对应层级 | 告诉你什么 |
|---|---|---|
| 速度表 | 业务层 | 车跑得快不快(QPS) |
| 乘客的等待时间 | 延迟层 | 体验好不好(P99) |
| 油量、转速、水温 | 资源层 | 消耗了什么(CPU / 内存 / GC) |
| 水温警告灯、油量警示灯 | 饱和度层 | 离出问题还有多远 |
关键洞察:速度和等待时间都是「已经发生的结果」,而警告灯是「即将发生问题的预警」。
- 速度掉到 20 km/h 时,问题已经发生了,你只能被动应对。
- 水温警告灯亮起时,你还有时间靠边停车。
性能工程里最值钱的指标,是那些「用户还没受影响,但系统已经在告警」的指标。 这就是饱和度层。
二、四层指标详解
第 1 层:业务层 —— 「服务好不好用」
| 指标 | 含义 | 常见坑 |
|---|---|---|
| QPS / RPS | 每秒处理的请求数 | 分母应该是「成功完成」的请求 |
| 错误率 | 失败请求占比 | 必须与延迟一起看(否则会「优化」出更多错误) |
| 超时率 | 超时请求占比 | 与错误率分开统计(超时和报错的处理方式不同) |
| Apdex | 满意度指标(快 / 可接受 / 慢三档) | 阈值必须来自业务可感知边界,不能拍脑袋 |
回答的问题:用户现在体验如何?
缺陷:只知道「好不好」,不知道「为什么」。
第 2 层:延迟层 —— 「用户等了多久」
| 指标 | 含义 |
|---|---|
| P50 / P90 / P95 / P99 / P999 | 各百分位的响应时间 |
| 最大值 | 极端情况(不适合做告警,因为波动太大) |
| 客户端延迟 vs 服务端延迟 | 两个不同的数字(见 1.3) |
回答的问题:用户的等待时间分布是什么样的?
缺陷:知道「慢」,不知道「慢在哪」。
第 3 层:资源层 —— 「消耗了什么」
| 类别 | 关键指标 | 说明 |
|---|---|---|
| CPU | user / sys / iowait / steal | sys 高通常意味着频繁系统调用、锁竞争或页错误;steal 高说明被虚拟机邻居抢了 CPU |
| 内存 | 堆使用、分配速率(MB/s) | 分配速率比堆占用更能预测 GC 压力 |
| GC | 次数、停顿时间、各代回收频率 | 停顿直接体现在延迟尾部 |
| 磁盘 | IOPS、吞吐、await | 「利用率 100%」不等于慢,要看 await |
| 网络 | 带宽、连接数、重传 | — |
回答的问题:为了达到现在的性能,消耗了什么?
缺陷:CPU 高不代表用户受影响(可能是在做有用的工作);CPU 低也不代表没事(可能在等 IO)。
第 4 层:饱和度层 —— 「离上限还有多远」⭐
| 资源 | 饱和度指标 | 危险信号 |
|---|---|---|
| 数据库连接池 | active / idle / pending |
pending > 0 持续存在 = 已经在排队 |
| 线程池 / 协程调度器 | 队列深度、活跃线程数 | 队列持续非空 |
| 队列(消息、任务) | 队列长度、消费延迟 | 持续增长 = 消费速度跟不上 |
| 缓存 | 命中率、容量占用 | 命中率下降、频繁驱逐 |
| 容器 | CPU 节流计数(nr_throttled) |
持续增长 = 被配额周期性掐停 |
| 磁盘 | IO 队列深度 | 队列长 = await 即将飙升 |
回答的问题:我离出问题还有多远?
这一层为什么最重要:
- 它比延迟更早报警。 连接池 pending 开始上升时,延迟可能只涨了 5%;等延迟明显恶化时,你已经没有调整时间了。
- 它直接指向瓶颈。 「pending > 0」直接告诉你「问题在连接池」,不需要再猜。
- 它不受协调遗漏影响。 延迟数据可能被压测方式污染(第 0 章),但「队列里积压了多少」是一个客观事实。
三、缺层的后果:一个排查失败的例子
背景:一个订单查询接口,P99 从 80 ms 涨到 400 ms。
只有业务层 + 延迟层(大多数团队的现状)
观察到的数据:
QPS:稳定在 600(没变)
P99:80ms → 400ms(恶化 5 倍)
CPU:45%(不高)
结论:CPU 不高、QPS 没变,但延迟涨了。不知道问题在哪。 接下来只能靠猜:是不是数据库慢了?是不是有慢查询?是不是网络问题?——于是开始了漫长的试探。
四层齐全
业务层:QPS 600(稳定),错误率 0.1%(稳定)
延迟层:P99 80ms → 400ms
资源层:CPU 45%,GC 停顿正常,磁盘 await 正常
饱和度层:数据库连接池 pending 从 0 → 18 ⭐
结论立刻清晰:
- CPU 不高 + 连接池排队 → 不是「算不过来」,而是「等数据库的时间变长了」。
- 下一步该查的是:数据库侧的执行计划、锁等待、或者连接池大小。
这一步省下的是几个小时。 而且注意:这个结论不是从延迟数据推出来的,而是从饱和度数据推出来的。
四、一个容易忽略的点:指标要「可分解」
四层指标齐全还不够,还要能沿调用链分解。否则你会知道「数据库类请求整体变慢了」,但不知道是哪一条 SQL。
建议的最小分解粒度:
接口层:http_server_requests{uri="/orders/{id}"}
├── 依赖层:dep.postgres{operation="findById"}
├── 依赖层:dep.redis{operation="get"}
└── 依赖层:dep.downstream{service="user-service"}
有了这个结构,才能算出第 1.3 节 要讲的「自身开销 = 本层延迟 − 各依赖延迟之和」。
一个经验法则:如果你无法回答「这个接口的 P99 里,数据库占了多少毫秒」,说明你的埋点粒度不够。
五、本节小结
- 指标分四层:业务 / 延迟 / 资源 / 饱和度,缺一层就会卡在排查半路。
- 饱和度层是最值钱的一层:它是领先指标,在用户受影响之前就报警,而且不受压测方式污染。
- 只有业务层和延迟层时,你只能知道「慢」,无法知道「为什么慢」。
- 指标还要能沿调用链分解(至少拆到依赖级),否则定位不到具体环节。
- 「资源层正常 + 饱和度层排队」这个组合,直接指向「不是算不过来,而是在等」——这是最常用的一条判读。
六、自测
- 某服务的 CPU 是 30%,P99 是 500 ms(SLO 要求 200 ms)。请说出你接下来要看的三个具体指标,以及每个指标能排除/确认什么假设。
- 为什么说「饱和度指标不受协调遗漏影响」?延迟指标受什么影响?
- 一个团队只监控 QPS、错误率、P99、CPU 这四项。请指出他们缺了哪一层,以及在一次「数据库变慢」的故障中,这个缺失会带来什么具体后果。
- ① 数据库连接池的 pending / active:如果
pending > 0,说明在排队,瓶颈可能是连接数不足或查询变慢;如果 pending 一直是 0,则排除连接池。② GC 停顿时间(P99)与分配速率:若 GC 停顿的尖刺与 P99 尖刺在时间上对齐,则 GC 是嫌疑;若不对齐,可排除。③ 依赖级延迟(dep.postgres 的 P99):如果它占了总延迟的绝大部分,问题在数据库;如果它很小而总延迟很大,问题在应用自身(序列化、锁、CPU 密集计算)。还可以看 wall-clock 火焰图确认「时间花在等待还是计算」。 - 饱和度指标记录的是「此刻队列里积压了多少」「池子里有多少在等」这类客观状态量,它由服务端的真实状态决定,与压测工具怎么发请求无关。而延迟指标是「观测到的样本统计量」——闭环压测会在服务端变慢时减少发压,导致慢样本没被采集到,于是 P99 被低估。饱和度不受这个影响,因为积压是真实发生的。
- 缺了饱和度层(具体说:连接池 pending、队列深度)。后果:「数据库变慢」时,连接池很快会被占满并开始排队,但如果没有 pending 指标,你只能看到「延迟上升、CPU 不高」——这个组合无法区分「数据库慢」「连接池太小」「下游阻塞」三种情况。你会去查数据库日志、看慢查询、翻应用代码,而实际上最直接的答案(连接池排了 18 个)就在你手边。更糟的是,你可能会错误地加大连接池,把压力转移到数据库,让问题恶化。