1.3 测量点:同一个请求,三个不同的数字
上一节:1.2 百分位不可平均 | 下一节:1.4 SLO 的四要素写法 配套代码:01-metrics-and-slo/03-measurement-points
一句话结论
「这个接口要 200 ms」这句话,缺了最重要的一个信息:在哪测的。 客户端测出 200 ms、网关测出 120 ms、服务端 handler 内部测出 30 ms——三个数字都是对的,但它们回答的是不同的问题。混着用,就会得出错误结论。
二、用快递物流理解「测量点」
你在网上买了一件商品,想知道「多久能到」。可以打卡的地方有很多:
| 打卡点 | 记录的时间 | 用户关心吗 |
|---|---|---|
| 你下单的时刻 | 起点 | ✅ 用户从这里开始等 |
| 仓库开始打包 | 分拣耗时 | ❌ 用户不关心,但仓管关心 |
| 打包完成、交给快递 | 打包耗时 | ❌ 但能定位「是不是仓库慢」 |
| 快递在路上 | 运输耗时 | 部分关心 |
| 快递到达你所在城市 | 分拨耗时 | ❌ |
| 快递员按门铃、你签收 | 终点 | ✅ 用户等的是这个 |
「送达需要 3 天」这句话:
- 从「下单」算:3 天。这是用户体验。
- 从「出库」算:2.5 天。这是物流公司内部的绩效。
- 从「到达本市」算:6 小时。这是末端配送的效率。
三个数字都对,但你不能拿「末端配送 6 小时」去论证「用户 3 天能收到」。
后端的测量点完全同理。
三、三个测量点
3.1 客户端(压测工具 / 真实用户浏览器 / App)
延迟 = 收到响应的时刻 − 发出请求的时刻
包含:客户端排队 + 网络往返 + 网关 + 服务排队 + 服务处理 + 响应传输。
- ✅ 这是最接近用户体验的数字。
- ❌ 受客户端本身影响(压测工具 GC、网卡、临时端口耗尽)。
- ❌ 如果压测工具用了闭环模型,这个数字会被协调遗漏污染(第 0 章)。
用途:验收 SLO、回答「用户等了多久」。
3.2 网关 / 负载均衡
延迟 = 网关转发响应的时刻 − 网关收到请求的时刻
包含:网关内部处理 + 服务排队 + 服务处理(不含客户端与部分网络)。
- ✅ 去掉了客户端噪声,适合长期监控。
- ✅ 能看到网关自身的开销(TLS 握手、鉴权、限流)。
- ❌ 看不到客户端的网络情况。
用途:生产环境长期观测、网关性能评估。
3.3 应用内埋点(服务端 handler)
延迟 = handler 返回的时刻 − handler 被调用的时刻
只包含服务自身的处理时间,不含网络、不含上游排队。
- ✅ 最细,可以进一步拆到每个依赖调用。
- ✅ 是唯一能回答「这 200 ms 花在哪了」的测量点。
- ❌ 明显小于用户感知的延迟,不能用来验收 SLO。
用途:定位瓶颈、做延迟分解。
四、三个数字的差额,就是你的线索
这是本节最有用的一句话:
客户端延迟 − 网关延迟 = 客户端排队 + 网络往返
网关延迟 − 应用内延迟 = 网关处理 + 服务排队
应用内延迟 − Σ依赖延迟 = 应用自身开销(序列化、计算、锁)
一个具体例子
| 测量点 | P99 |
|---|---|
| 客户端 | 200 ms |
| 网关 | 130 ms |
| 应用内(handler) | 40 ms |
| └ 依赖:PostgreSQL | 25 ms |
| └ 依赖:Redis | 3 ms |
| └ 依赖:下游 HTTP | 5 ms |
逐层做减法:
| 差额 | 数值 | 含义 |
|---|---|---|
| 200 − 130 = 70 ms | 客户端到网关 | 网络 + 客户端排队。这部分你优化代码没用 |
| 130 − 40 = 90 ms | 网关到应用 | 网关处理 + 服务排队(进不了 handler,说明在等 CPU/线程/连接) |
| 40 − (25+3+5) = 7 ms | 应用自身 | 序列化 + 业务逻辑。这才是你写的代码 |
结论:
- 你写的代码只占 7 ms(3.5%)。
- 最大的两块是「网络」和「排队」。
- 如果只优化业务代码,最多也只能省 7 ms。
这就是为什么「先测量、后优化」如此重要——大多数人的直觉会让他们去优化那 7 ms。
五、四个必须避免的错误
❶ 拿不同测量点的数据做对比
第 1 周(客户端测得):P99 = 200 ms ← 基线
第 2 周(服务端测得):P99 = 45 ms ← "优化后"
结论:性能提升了 77%
这个结论毫无意义——两个数字测的根本不是同一件事。优化前后必须用同一个测量点。
❷ 用应用内延迟验收 SLO
应用内 P99 = 45 ms,看起来完美,但用户实际等了 200 ms。SLO 必须用客户端(或至少网关)视角来定义。
❸ 忽略「服务排队」这一段
「网关 130 ms 减去应用 40 ms = 90 ms」——这 90 ms 里很大一部分是请求在排队等待被处理(等线程、等 CPU、等协程调度)。这部分延迟不在任何一段代码里,所以你在代码里怎么找都找不到。
这正是第 0 章 0.5 节讲的排队效应。 只有饱和度指标(队列深度、池 pending)能看到它。
❹ 埋点位置不对
常见错误:
- 在过滤器/中间件里计时,但中间件之后还有序列化 —— 漏掉了序列化开销。
- 只统计成功请求(用成功数做分母)—— 最慢的超时请求恰好被剔除。
- 在异步/协程场景下,计时器跨越了挂起点但没考虑调度时间(有时这是想要的,有时不是,必须明确)。
六、本节小结
- 同一个请求至少有三个都正确的测量点:客户端、网关、应用内。它们回答不同的问题。
- 三个数字的差额是最有价值的线索:客户端−网关 = 网络;网关−应用 = 排队 + 网关处理;应用−依赖 = 你自己的代码。
- SLO 必须用客户端(或网关)视角定义;应用内延迟只用于定位。
- 优化前后必须用同一个测量点,否则对比无效。
- 「服务排队」这段延迟不在任何代码里,只能通过饱和度指标发现。
七、自测
- 某接口客户端 P99 = 300 ms,网关 P99 = 280 ms,应用内 P99 = 60 ms。请说出你接下来最该查的两件事,并说明理由。
- 一个团队报告「接口优化成功,P99 从 250 ms 降到 60 ms」。你追问哪两个问题就能判断这个结论是否可信?
- 为什么「应用自身开销 = 应用内延迟 − 各依赖延迟之和」这个减法,有时会算出负数?
- ① 查网关到应用之间的 220 ms(280 − 60)去哪了。这个差额巨大,最可能是服务排队(等线程/连接/CPU/协程调度)或网关自身处理慢。要查:网关的 CPU 与连接数、应用的饱和度指标(线程池队列、连接池 pending)、容器 CPU 节流。② 查网关内部的 20 ms(300 − 280):如果是 TLS 握手、鉴权调用或限流计算,相对影响小,优先级低于第一项。注意:应用自身只占 60 ms 的一小部分,优化业务代码收益有限。
- ① 测量点是什么?优化前后是否一致? 如果基线是客户端测得、优化后是服务端测得,那 250→60 是测量点的差别,不是优化效果。② 负载前提与样本量是什么? 多少 QPS、什么数据量、跑了几轮、错误率有没有变化(可能把慢请求变成了错误请求)。
- 因为并行调用。如果应用内同时调用 PostgreSQL(25 ms)和 Redis(3 ms),总耗时不是 28 ms 而是
max(25, 3) = 25 ms。此时「40 − (25+3) = 12」就低估了自身开销;如果依赖调用之间有重叠、或者计时器覆盖范围与依赖计时器不一致,甚至可能算出负数。正确做法:串行链路上可以直接相减;并行链路上要用实际的关键路径(或直接用 tracing 的 span 树)来分析。