延迟不存单值,存一叠累积水桶(_bucket + le 标签);histogram_quantile 在桶间插值算分位数,topk 只画前 N 名治毛线团。覆盖 c19 / c15 / c23 / b4 / m109 / m123 等全部「分位数面板」的机制课。
系统不记录每个请求的耗时(太贵),只维护一叠累积水桶:「≤10ms 的有多少个、≤50ms 的有多少个、≤1s 的有多少个……」每只桶是一个 Counter,标签 le(less-or-equal)就是桶的天花板。histogram_quantile 拿着这叠桶回答「第 950 名排在哪」——在相邻两只桶之间插值估出 P95。
代价是 P95 只是估算值(桶越粗越不准);收益是十几个计数器就能算任意分位数。而当你把几十个接口的分位数曲线全部画出来,图会糊成毛线团——topk(10, …) 就是急救剪:只画最慢的前 10 条(c19 口径),排行榜立刻清爽。另外记住第一大坑:算分位数时 by (le) 万万不能丢,丢了桶就被加成一条线,分位数直接失明。
学校发成绩单从不公布每个人的分数,只公布分数段人数表:「60 分以下 5 人、80 分以下 38 人、100 分以下 40 人」。注意这张表不用存 40 个人的分数,只存 3 个计数——却足以回答「前 95% 的分界线大概在哪」:第 38 名在 80 分档里,第 36~38 名之间的分界线一插值就估出来了。直方图(Histogram)就是这张分数段人数表,每档是一只桶,桶里是累计人数。
为什么监控要这么省?因为「把每个请求的耗时都存下来」代价巨大——一秒钟几千个请求,存一周就是天文数字。分桶只要十几只计数器,就能随时算 P50/P95/P99 任意分位数。代价你现在已经知道:桶之间的细节丢了,P95 是插值估出来的,桶越粗越只能看大概。四两的字节分布面板(m123)干脆 p50/p95 一起看,就是为了对冲估算误差。
实操中有两个必杀技。第一是公式骨架:histogram_quantile(0.95, sum(rate(…_bucket[5m])) by (le, 维度))——先 rate(桶是 Counter)、再 sum by le(保留天花板标签)、最后插值;le 忘了 by 就全毁。第二是 topk:几十个 handler 各来几条线,图糊得没法看——topk(10, …) 每个时刻只留最慢的前 10 名,c19「P99 Top 10 慢接口」就是 topk 和 histogram_quantile 的合体技。
| 类比里的东西 | 系统里对应的东西 |
|---|---|
| 分数段人数表(60 分以下 5 人…) | _bucket 指标:le="0.05" 的桶里有 950 个(累积计数) |
| 每一档的上沿(「80 分以下」) | le 标签(less-or-equal):每只桶的天花板 |
| 用分段人数推算分界线 | histogram_quantile(0.95, …) 在相邻桶之间插值 |
| 档越细推算越准 | 桶越密 P95 越准;桶粗只能估大概(m123 用 p50/p95 对照) |
| 热搜榜只看前 10 | topk(10, …):毛线团里只画最慢的前 10 条(c19/b4 口径) |
siliang_http_request_duration_seconds_bucket{job="siliang_backend_worker"}:同一 handler 冒出一叠 le 不同的序列(le="0.005"、le="0.01"…le="+Inf"),这就是水桶堆。histogram_quantile(0.95, sum(rate(...[5m])) by (le)):一叠桶变成一条 P95 曲线。topk(10, histogram_quantile(0.99, … by (le, handler))):把 10 改成 3 更清爽,改回 10 恢复榜单。_bucket 就是它的身份证。
# 一只桶的样子:le 是天花板,值是累计个数 siliang_llm_call_duration_seconds_bucket{le="0.5"} # 含义:耗时 ≤ 0.5 秒的调用累计发生了多少个
le="+Inf"(全部样本)。
# 累积关系:只增不减的套娃 le="0.05" → 950 le="1" → 990 # 包含上面那 950 个 le="+Inf" → 1000 # 全部
histogram_quantile(分位, sum(rate(…_bucket[窗口])) by (le, 维度))——rate 因为桶是 Counter;sum by (le, …) 因为要跨 worker 汇总但必须保留 le;最后插值。
# c15 口径:按 model 拆的 LLM 延迟分位数(单位:秒) histogram_quantile(0.95, sum(rate(siliang_llm_call_duration_seconds_bucket[5m])) by (le, model))
# 错: histogram_quantile(0.95, sum(rate(…_bucket[5m]))) # → 鬼数字 # 对: histogram_quantile(0.95, sum(rate(…_bucket[5m])) by (le, handler))
# 错: histogram_quantile(0.95, sum(…_bucket) by (le)) # → 开机以来 # 对: histogram_quantile(0.95, sum(rate(…_bucket[5m])) by (le))
# m123 口径:xAI 生图响应字节 p50/p95 对照 histogram_quantile(0.95, sum(rate(siliang_xai_image_response_bytes_bucket[5m]))) histogram_quantile(0.50, sum(rate(siliang_xai_image_response_bytes_bucket[5m])))
topk(N, 表达式) 每个时刻只保留值最大的前 N 条序列。放在最外层就是排行榜:c19 取最慢 10、b4 取最慢 5。
# c19 完整口径:全站最慢 10 个接口的 P99 topk(10, histogram_quantile(0.99, sum(rate(siliang_http_request_duration_seconds_bucket[5m])) by (le, handler)))
# 标准姿势:job 过滤放花括号里 sum(rate(siliang_http_request_duration_seconds_bucket{job="siliang_backend_worker"}[5m])) by (le, handler)
① HTTP P99 延迟(Top 10 慢接口)(c19)——topk 与 histogram_quantile 的合体样板,你的性能优化待办清单:🟢 HTTP P99 延迟(Top 10 慢接口)↗
② LLM 调用延迟 P50/P95/P99(按 model)(c15)——by (le, model) 的教科书:gpt-5 长输出 60s+ 属正常:🟡 LLM 调用延迟 P50/P95/P99 ↗
③ WS 连接持续时长 P50/P95/P99(c23)——by (le) 全站一条线,P99 异常长 = 僵尸连接线索:🔵 WS 连接持续时长 ↗
④ biz HTTP 延迟 P95(Top 5)(b4)——topk(5, …) 聚焦版;要 P99 就把 0.95 改成 0.99:🟢 HTTP 延迟 P95(Top 5 慢接口)↗
⑤ 文件/媒体字节分布 p95(m109)——直方图不只量时间:ZIP 产物/视频转存/TTS 音频的「典型体重」也是桶,解释 RSS 为什么冲高:📏 文件/媒体处理字节分布 p95 ↗
c19 的完整口径,可直接抄进 Grafana;换 b4 的指标名就是 biz 盘版本。
# 全站最慢 10 个接口(P99,单位:秒,c19 口径) topk(10, histogram_quantile(0.99, sum(rate(siliang_http_request_duration_seconds_bucket{job="siliang_backend_worker"}[5m])) by (le, handler))) # 判读:榜首长期霸榜 = 最优先优化;名次变化 = 优化有效/退化
排行榜锁定嫌疑人后,把这一个接口的三条分位数拉出来看「队伍形状」。
# 固定 handler,三条分位数一起看(把 P95 榜首的 handler 填进来) histogram_quantile(0.50, sum(rate(siliang_http_request_duration_seconds_bucket{handler="/me"}[5m])) by (le)) histogram_quantile(0.95, sum(rate(siliang_http_request_duration_seconds_bucket{handler="/me"}[5m])) by (le)) histogram_quantile(0.99, sum(rate(siliang_http_request_duration_seconds_bucket{handler="/me"}[5m])) by (le)) # 判读:三线贴得近 = 均匀慢(容量问题);P99 独秀 = 长尾(锁/缓存/大参数)
不同模型的耗时分布天差地别,混在一起算分位数就是自欺欺人。
# 按 model 拆的 P95(单位:秒) histogram_quantile(0.95, sum(rate(siliang_llm_call_duration_seconds_bucket{job="siliang_backend_worker"}[5m])) by (le, model)) # 判读:gpt-5 长输出 60s+ 属正常;P50 整体抬升才是 provider 变慢/限流
RSS 无缘无故冲高?来字节分布面板对时间戳——谁在什么时候搬了大文件一对照便知。
# ZIP 产物的典型体重 P95(m109 口径之一) histogram_quantile(0.95, sum(rate(siliang_file_batch_archive_bytes_bucket{job="siliang_backend_worker"}[5m])) by (le)) # 用法:RSS 冲高时刻 × 字节分布对时间戳 = 找到搬运工; # 处理 200MB 视频时内存必先涨 200MB+
同一个分位数表达式,三个场景三种 N:巡检看前 3,排查看前 10,全量导出才不加 topk。
# 巡检大屏:只看前 3,一眼扫完 topk(3, histogram_quantile(0.99, sum(rate(siliang_http_request_duration_seconds_bucket[5m])) by (le, handler))) # 排查:前 10(c19 口径);全量导出给脚本分析:去掉 topk # 注意:topk 每个时刻独立选人,线条中途换 handler 属正常,看图例认名字
# 错: histogram_quantile(0.95, sum(rate(…_bucket[5m]))) # → 鬼数字 # 对: histogram_quantile(0.95, sum(rate(…_bucket[5m])) by (le, handler))
# 错: histogram_quantile(0.95, sum(…_bucket) by (le)) # → 开机以来 # 对: histogram_quantile(0.95, sum(rate(…_bucket[5m])) by (le))
# 错: avg by (le) (rate(…_bucket[5m])) # → 人数缩水 # 对: sum by (le) (rate(…_bucket[5m])) # → 人数合并
# 错: (P95_A + P95_B) / 2 # → 谁都不是 # 对: histogram_quantile(0.95, sum(rate(…_bucket[5m])) by (le)) # → 全站
# 错: 认死一条线的颜色当「同一个接口」 # → 名字会换 # 对: 看图例名;固定对象用 {handler="/me"} 过滤
# 错: histogram_quantile(95, …) # → 不是合法分位 # 对: histogram_quantile(0.95, …) # → P95
# 错: P95 不动 = 面板坏了? # → 桶粗,精度如此 # 对: p50/p95 双线看趋势(m123 口径),粗桶看形状足够
参考答案:学校不公布每个人的分数,只公布分数段人数表(60 分以下 5 人、80 分以下 38 人……)——3 个计数就能回答「前 95% 的分界线在哪」。延迟同理:存每个请求的耗时太贵,分桶只要十几只计数器就能算任意分位数。每档就是一只桶(le 是档位上沿),桶里存的是「≤ 该值的累计个数」。
参考答案:histogram_quantile(0.95, sum(rate(…_bucket[5m])) by (le, handler))。rate:桶是累计计数器(Counter),必须先算每秒进桶速度才反映当前;sum:把 4 个 worker 的桶合并,但 by 里必须保留 le——它是桶的天花板标签,丢了插值机就没原料(第一大坑);histogram_quantile:在相邻桶之间插值估出第 95% 位置。
参考答案:几十个 handler 的分位数曲线画在一张图上会糊成毛线团,topk(N, …) 每个时刻只保留值最大的前 N 条,把图变成排行榜(c19 前 10、b4 前 5)。换人是因为 topk 每个时刻独立评选:上一分钟 A 接口最慢、下一分钟 B 反超,线条的主人就换了——看图例认名字,固定对比用 handler 过滤。
参考答案:桶只记「≤ 上沿的有多少个」,桶内的分布细节全丢了,P95 只能在相邻两只桶之间插值估个大概——桶越粗越不准。四两的对冲方式:m123 用 p50/p95 双线对照看分布形状而不是迷信单点;趋势判断足够用,要精确值只能加密埋点桶。