P95 = 95% 的请求比它快。平均值会被海量快请求稀释,P99 会诚实暴露最慢的那撮人。覆盖 c15 LLM 延迟 / c19 P99 Top10 / c23 WS 时长 / c28 / b4 等全部延迟面板的原理课。
把一段时间内到达的所有请求按耗时从快到慢拉出来排队:P50 是队伍正中间那位(典型体验),P95 是站在 95% 位置的那位——95% 的请求都比它快,只有最慢的 5% 排它后面;P99 就是最倒霉的 1% 的水平。分位数不关心平均,只关心「队伍里某个位置的人等了多久」。
为什么不看平均值?因为平均值会被海量快请求稀释:999 个 100ms 的请求混进 1 个 60s 的 LLM 慢请求,平均只有约 160ms,看起来一切正常,其实有人等了整整一分钟。P99 会诚实地把这 60s 举过头顶。四两所有延迟面板(c15/c19/c23/c28/b4)全用分位数,没有一个用平均值。
想象一场 1000 人的马拉松。完赛后把所有人按成绩从快到慢排队:站在第 500 位的是「中位数选手」,他的成绩就是 P50——一半人比他快、一半人比他慢,是「典型选手」的水平;站在第 950 位的是 P95 选手——95% 的人都比他快,他是「最慢的那一批人」的代表;站在第 990 位的是 P99——垫底那 1% 的水平。赛事报告写「平均成绩 2 小时」毫无诊断价值,写「P99 选手跑了 4 小时」立刻引出问题:那 10 个人经历了什么?
服务器请求就是这场马拉松。大多数请求几十毫秒完赛,但 LLM 调用天生有长尾:gpt-5 长输出会跑到 60 秒以上。如果只报平均值,海量快请求把这 60s 稀释得无影无踪(999 个 100ms 加 1 个 60s,平均才 160ms)——报表绿色、用户爆碎。P99 不做平均,它直接指着队伍末尾问:最惨的这 1% 等了多久?
于是分位数成了「长尾照妖镜」:P50 正常而 P99 爆炸,说明大多数人没事、但有一小撮请求遇到了特殊情况——缓存没命中、撞上数据库锁冲突、或者带了个超大参数。四两的面板把三个分位数画在一起(c15 LLM 延迟、c23 WS 连接时长),就是让你一眼看出「队伍有多宽」:P50 和 P99 之间的距离,就是你的长尾宽度。
| 类比里的东西 | 系统里对应的东西 |
|---|---|
| 第 500 名选手的成绩 | P50(中位数):典型用户体验,一半请求比它快 |
| 第 950 名选手的成绩 | P95:95% 的请求比它快,最慢 5% 的「代表」 |
| 垫底 1% 的成绩 | P99:最倒霉的 1%,长尾问题的照妖镜 |
| 全员平均成绩 | 平均值:被海量快请求稀释,60s 慢请求根本拉不平它 |
| P50 正常而 P99 爆炸 | 长尾问题:一小撮请求撞上缓存未命中/锁冲突/大参数 |
histogram_quantile(0.50, …) 和 histogram_quantile(0.99, …)(c15 口径 by (le, model)):两条线的间距 = 长尾宽度。# 分位数在 PromQL 里的算式(下一课拆解内部机制) histogram_quantile(0.95, sum(rate(…_bucket[5m])) by (le, handler)) # 0.95 = 队伍 95% 位置
# P50 突然整体抬升 = 大多数人都变慢了(c15 判读口径) # → provider 变慢/限流,对照错误率面板互证
# 算给你看: # (999 × 0.1s + 60s) ÷ 1000 ≈ 0.16s ← 平均,岁月静好 # P99 ≈ 60s ← 真相,有人等了一分钟
# 下钻套路:全站 P99 抬升 → 按 handler 拆开找那撮人 histogram_quantile(0.99, sum(rate(siliang_http_request_duration_seconds_bucket[5m])) by (le, handler))
# 按 model 拆(c15 口径),单位:秒 histogram_quantile(0.99, sum(rate(siliang_llm_call_duration_seconds_bucket[5m])) by (le, model))
# c19 口径:按 handler 拆 P99,再取前 10 名 topk(10, histogram_quantile(0.99, sum(rate(…_bucket[5m])) by (le, handler)))
# 分位数的原料是 _bucket(一叠累积计数器) siliang_llm_call_duration_seconds_bucket{le="0.5"} # 没有 _bucket 就排不了队(第 7 条坑)
① LLM 调用延迟 P50/P95/P99(按 model)(c15)——典型体验看 P50,最惨的 1% 看 P99;gpt-5 长输出 60s+ 属正常:🟡 LLM 调用延迟 P50/P95/P99 ↗
② HTTP P99 延迟(Top 10 慢接口)(c19)——按 P99 排序的「性能优化待办清单」,优化前后变没变快看它:🟢 HTTP P99 延迟(Top 10 慢接口)↗
③ WS 连接持续时长 P50/P95/P99(c23)——一条对讲机平均开多久;P99 异常长 = 僵尸连接线索:🔵 WS 连接持续时长 ↗
④ 消耗统计查询耗时 P95(c28)——查账单要等多久,聚合 SQL 随数据量变慢会在 P95 先显形:🟠 消耗统计查询耗时 P95 ↗
⑤ biz HTTP 延迟 P95(Top 5)(b4)——biz 盘的优化清单,比核心盘 top10 更聚焦:🟢 HTTP 延迟 P95(Top 5 慢接口)↗
c19 就是干这个的:全站接口按 P99 排队,取最慢前 10。优化做完再看这张图验收。
# c19 完整口径:最慢 10 个接口的 P99(单位:秒) topk(10, histogram_quantile(0.99, sum(rate(siliang_http_request_duration_seconds_bucket{job="siliang_backend_worker"}[5m])) by (le, handler))) # 判读:榜首长期不动 = 最值得投入的优化点; # 优化后名次下滑 = 改出了效果
P99 高不一定是故障:先分 model,再看是「整体抬升」还是「只有长输出正常地长」。
# P50 与 P99 都按 model 拆(c15 口径,单位秒) histogram_quantile(0.50, sum(rate(siliang_llm_call_duration_seconds_bucket[5m])) by (le, model)) histogram_quantile(0.99, sum(rate(siliang_llm_call_duration_seconds_bucket[5m])) by (le, model)) # 判读:P99 60s+ 但 P50 稳定 = gpt-5 长输出,正常; # P50 突然整体抬升 = provider 变慢/限流,对照 c13 错误率互证
延迟告警盯分位数而不是平均值。阈值是业务承诺(SLA),示例值按你的业务定,四两图谱未定统一延迟阈值。
# P95 延迟告警模板(示例阈值,按业务 SLA 填) groups: - name: siliang-latency rules: - alert: HandlerP95Slow expr: | histogram_quantile(0.95, sum(rate(siliang_http_request_duration_seconds_bucket{job="siliang_backend_worker"}[5m])) by (le, handler)) > 2 # 示例:P95 超 2 秒 for: 10m labels: severity: warning annotations: summary: "{{ $labels.handler }} P95 超标,进 c19 榜单核实"
全站 P99 抬升时,第一步是按 handler 拆开——通常是某一个接口在拉仇恨。
# 第一步:拆 handler 看谁在拉高全站(c19 不带 topk 的版本) histogram_quantile(0.99, sum(rate(siliang_http_request_duration_seconds_bucket[5m])) by (le, handler)) # 第二步:锁定 handler 后缩小时间窗对比抬升前后(对齐时间戳) # 第三步:按病因三连排查——缓存命中(m127)/锁冲突(c34/c35)/大参数
「感觉快了」不算数,分位数说话:同一查询、两个时间窗,P95 降了多少清清楚楚。
# 优化前(把时间选择器拨到发布前 1h) histogram_quantile(0.95, sum(rate(siliang_http_request_duration_seconds_bucket[5m])) by (le, handler)) # 优化后(发布后 1h,同一查询原样再跑一遍) # 注意流量要可比:挑流量量级相近的两个时段,否则又是平均值的把戏
# 错: 平均 160ms,一切正常 # → 稀释陷阱 # 对: P50 100ms / P99 60s,长尾待查 # → 诚实
# 错: 面板只画 P50 # → 长尾全盲 # 对: P50/P95/P99 三线同图(c15/c23 口径)
# 错: P99 60s = 出事了,全组排查 # → 可能是常态 # 对: by (model) 拆开,P50 抬升才立案 # → c15 判读口径
# 错: (P95上午 + P95下午) / 2 # → 谁都不是 # 对: histogram_quantile(0.95, sum(rate(...[1d])) by (le, handler))
# 错: P95 是「95% 请求的延迟值」 # → 概念错 # 对: P95 = 95% 的请求比它快 # → 排队位置
# 错: sum(rate(…_bucket[5m])) # → 维度没了 # 对: sum(rate(…_bucket[5m])) by (le, handler) # → 每接口一条
# 错: histogram_quantile(0.95, siliang_db_pool_connections) # → 排不了队 # 对: histogram_quantile(0.95, sum(rate(…_duration_seconds_bucket[5m])) by (le))
参考答案:把所有请求想象成 1000 个跑完的选手,按成绩从快到慢排队。P95 就是站在第 950 位那位选手的成绩——95% 的人都比他快,他代表「最慢的那一批」。所以「P95 延迟 2 秒」的意思是:95% 的请求 2 秒内就返回了,只有 5% 比它更慢。
参考答案:999 个请求只花了 100ms,1 个请求等了 60s。平均把 60s 摊到 1000 个人头上:(999×0.1 + 60) ÷ 1000 ≈ 160ms——快请求把灾难稀释了。平均值对「个体体验」没有意义,就像马云进酒吧人均身价上百亿。要看见那 1 个用户,只能看 P99。
参考答案:这是典型的长尾问题——大多数人没事,一小撮请求很惨。下钻顺序:① 按 handler 拆开(c19 口径 by le, handler)找出拉高 P99 的接口;② 按三大病因排查:缓存命中(m127)、数据库锁冲突(c34/c35)、大参数/大响应;③ 修复后回到 c19 榜单看名次变化验收。
参考答案:不一定是。c15 的口径写明:gpt-5 长输出跑到 60s+ 属正常(单位秒)。判断方法:按 model 拆开看——只有长输出的模型 P99 高、P50 稳定,是常态;如果 P50 突然整体抬升,说明大家都变慢了,才是 provider 变慢或限流,此时对照 c13 错误率互证后再动手。