1.8 案例解剖:P99 达标,用户仍然不满
一句话结论
「指标达标」和「用户满意」之间的差距,通常不是测量精度问题,而是四个具体的方法错误。 这一节把第 0、1 章的所有检查点,装进一个完整的排查故事里。
一、案例背景
产品经理的投诉:
「我们的订单详情页最近用户投诉变多了,说很卡。但你们的监控显示 P99 只有 180 ms,完全达标啊?」
技术团队手上的数据:
| 指标 | 数值 | 状态 |
|---|---|---|
| 订单查询接口 P99(服务端埋点) | 180 ms | ✅ 达标(SLO 是 200 ms) |
| 错误率 | 0.05% | ✅ |
| QPS | 850 | ✅ |
| CPU | 55% | ✅ |
表面上一切正常,但用户确实在抱怨。 接下来是排查过程。
二、四个原因,逐个揪出来
原因 ❶:只统计了成功请求
检查动作:看延迟统计的分母是什么。
// 问题代码
val timer = Timer.builder("http.server.requests").register(registry)
timer.record(duration) { /* 只有走到这一步的请求才被记录 */ }
// 请求在超时或抛异常时,根本没有走到 record()
发现:这个接口有 3% 的请求在 2 秒后超时并被网关中断,但这些请求根本没进入延迟统计——它们被算成了「错误」,从延迟样本中消失了。
结果:统计里只剩「又快又成功」的请求,P99 自然漂亮。
| 真实情况 | 报告中的情况 |
|---|---|
| 85% 请求 < 100 ms | 85% 请求 < 100 ms |
| 12% 请求 100–300 ms | 12% 请求 100–300 ms |
| 3% 请求 > 2000 ms(超时) | 被剔除,不计入延迟 |
| P99(含超时)≈ 2100 ms | P99(不含超时)= 180 ms |
修正:延迟统计的分母必须是总请求数,超时和错误的请求也要计入分布。
原因 ❷:只测了单个接口,而用户经历的是整条链路
检查动作:看用户的一次操作实际会调用几个接口。
发现:订单详情页需要并行调用 5 个服务:
订单详情页
├── 订单服务(P99 = 180 ms)
├── 用户服务(P99 = 120 ms)
├── 商品服务(P99 = 150 ms)
├── 物流服务(P99 = 200 ms)
└── 优惠券服务(P99 = 90 ms)
每个服务单独看都达标。但整页的体验取决于最慢的那个。用第 0 章的公式估算:
每个服务「慢」的概率 ≈ 1%
5 个服务中至少一个慢的概率 = 1 - 0.99^5 ≈ 4.9%
也就是说:接近 5% 的页面加载是慢的。 而每个服务的监控都显示「99% 的请求正常」。
修正:
- 增加一个端到端的 SLO(页面级或网关级),而不只是单接口 SLO;
- 监控「一次用户操作触发的所有请求」的最大延迟;
- 用 tracing 的 span 树看真实的关键路径。
原因 ❸:压测环境与生产不一致
检查动作:对照第 1.7 节 的九项清单,逐项核对压测环境与生产。
| 项目 | 压测环境 | 生产环境 | 影响 |
|---|---|---|---|
| 容器 CPU limit | 无限制 | 1 核 | 🔴 生产会被周期性节流 |
| 数据量 | 5 万行 | 800 万行 | 🔴 执行计划不同 |
| 数据分布 | 均匀 | 幂律(热点集中) | 🔴 缓存命中率差 |
| 副本数 | 1 | 6 | — |
| JVM 堆 | 4g | 2g | 🔴 GC 频率差一倍 |
发现:压测时用的是「本地开发机 + 无限制 CPU + 5 万行数据」,而生产是「容器 1 核 limit + 800 万行数据」。
修正:压测环境的容器配置、数据量级、数据分布必须与生产一致。
原因 ❹:测量点写错了
检查动作:确认 SLO 的测量点。
发现:SLO 写的是「P99 < 200 ms」,测量点是应用内埋点——只测了 handler 内部的时间,不含网关排队和网络。
而用户实际经历的:
| 环节 | P99 |
|---|---|
| 客户端到网关 | 40 ms |
| 网关处理 | 25 ms |
| 服务排队 | 60 ms |
| handler 内部 | 180 ms |
| 客户端实际体验 | ≈ 305 ms |
修正:SLO 的测量点必须与「用户感知」对齐——用客户端或至少网关视角来定义 SLO,应用内埋点只用于定位。
三、修正后的数据
把四个问题都修掉后,重新测量:
| 指标 | 修正前(报告值) | 修正后(真实值) |
|---|---|---|
| P99(含超时、客户端视角) | 180 ms | 2100 ms |
| 错误率(含超时) | 0.05% | 3.2% |
| 页面级成功率(5 个服务全快) | — | 95.1% |
| P99(按 800 万行 + 容器 limit 压测) | 180 ms | 420 ms |
结论:真实的性能问题一直存在,只是被四层「测量方法错误」挡住了。
注意:这四个原因没有一个是「代码写得不好」。全都是「测错了」。这也是为什么第 0 章要把观念放在工具之前。
四、从案例提炼的检查清单
每次看到「指标达标」,过一遍这五条:
- 分母对不对? 延迟统计是否包含超时与失败的请求?
- 测量点对不对? SLO 用的是客户端/网关视角,还是只看应用内?
- 链路范围对不对? 是单接口指标,还是用户实际经历的组合?
- 环境一致吗? 容器 limit、数据量、数据分布、JVM 参数是否与生产一致?
- 样本可信吗? 跑了几轮?预热了吗?是开环还是闭环模型?
只要有一条答不上来,这个「达标」就不能采信。
五、本节小结
- 「指标达标但用户不满」的四个典型原因:只统计成功请求、只测单接口、环境不一致、测量点写错。
- 这四个问题都不是代码问题,而是测量方法问题——所以优化代码解决不了。
- 修正后的真实数字,往往比报告值差一个数量级。
- 用上面那五条检查清单做验收前的例行检查。
六、自测
- 用本节的四个原因,解释「为什么错误率上升时,延迟指标反而可能变好」。
- 一个页面调用 8 个下游服务,每个服务的「慢请求率」是 0.5%。这个页面「至少有一次慢」的概率大约是多少?如果要把这个概率控制在 2% 以内,每个服务的慢请求率上限大约是多少?
- 案例中「服务排队 60 ms」这一项,用第 1.1 节的四层指标,应该用哪些指标去监控和验证?
- 因为延迟统计的分母通常是「成功完成的请求」。当一部分慢请求变成超时或错误后,它们就从延迟样本中消失了——剩下的都是"又快又成功"的请求。于是 P50/P99 都会变好,但实际上系统更差了(更多用户拿到了错误或超时)。所以延迟和错误率必须一起看,且延迟的分母要用总请求数。
- 每个服务正常的概率是 99.5%,8 个全部正常的概率 =
0.995^8 ≈ 0.9607,所以「至少一次慢」的概率 ≈ 3.9%。要控制在 2% 以内:(1-p)^8 ≥ 0.98→1-p ≥ 0.98^(1/8) ≈ 0.99748→p ≤ 0.25%。(这个计算说明:下游越多,对每个下游的要求越苛刻——这是微服务拆分的隐性成本。) - 用饱和度层指标:① 线程池/协程调度器的队列深度;② 数据库连接池的
pending;③ 容器 CPU 节流计数(nr_throttled)。辅助用 Little’s Law 反推:排队时间 = 在途请求数 / 到达率 − 服务处理时间。「排队」这个环节没有对应的代码,所以只能靠饱和度指标 + 业务层与延迟层的差额来发现(见 1.3 节 的差额分析法)。