文档目录

1.8 案例解剖:P99 达标,用户仍然不满

上一节:1.7 实验元数据 | 下一节:1.9 Lab 1


一句话结论

「指标达标」和「用户满意」之间的差距,通常不是测量精度问题,而是四个具体的方法错误。 这一节把第 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 参数是否与生产一致?
  • 样本可信吗? 跑了几轮?预热了吗?是开环还是闭环模型?

只要有一条答不上来,这个「达标」就不能采信。


五、本节小结

  1. 「指标达标但用户不满」的四个典型原因:只统计成功请求、只测单接口、环境不一致、测量点写错。
  2. 这四个问题都不是代码问题,而是测量方法问题——所以优化代码解决不了。
  3. 修正后的真实数字,往往比报告值差一个数量级。
  4. 用上面那五条检查清单做验收前的例行检查。

六、自测

  1. 用本节的四个原因,解释「为什么错误率上升时,延迟指标反而可能变好」。
  2. 一个页面调用 8 个下游服务,每个服务的「慢请求率」是 0.5%。这个页面「至少有一次慢」的概率大约是多少?如果要把这个概率控制在 2% 以内,每个服务的慢请求率上限大约是多少?
  3. 案例中「服务排队 60 ms」这一项,用第 1.1 节的四层指标,应该用哪些指标去监控和验证?
  1. 因为延迟统计的分母通常是「成功完成的请求」。当一部分慢请求变成超时或错误后,它们就从延迟样本中消失了——剩下的都是"又快又成功"的请求。于是 P50/P99 都会变好,但实际上系统更差了(更多用户拿到了错误或超时)。所以延迟和错误率必须一起看,且延迟的分母要用总请求数。
  2. 每个服务正常的概率是 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%。(这个计算说明:下游越多,对每个下游的要求越苛刻——这是微服务拆分的隐性成本。)
  3. 用饱和度层指标:① 线程池/协程调度器的队列深度;② 数据库连接池的 pending;③ 容器 CPU 节流计数(nr_throttled)。辅助用 Little’s Law 反推:排队时间 = 在途请求数 / 到达率 − 服务处理时间。「排队」这个环节没有对应的代码,所以只能靠饱和度指标 + 业务层与延迟层的差额来发现(见 1.3 节 的差额分析法)。