4. PromQL 进阶:Histogram 分位数计算、聚合操作、子查询、Recording Rules 优化
PromQL 学到这,基本查询和简单聚合你肯定没问题了。但真正到了生产环境,你会发现——光会查平均值,根本定位不了问题。
举个例子,你的 API 平均延迟是 200ms,看起来还行。但用户投诉说“卡死了”。为什么?因为那 5% 的慢请求把平均值拉高了,但平均值掩盖了真相。这时候,你需要的是 分位数。
这一章,我们就啃下 PromQL 里最硬核的几块骨头:Histogram 分位数、聚合操作、子查询,还有 Recording Rules 优化。嗯,都是我在生产线上摔过跟头才搞明白的东西。
核心观点:PromQL 的威力不在单条查询,而在组合。分位数告诉你“慢在哪”,聚合告诉你“谁在慢”,子查询告诉你“为什么慢”,Recording Rules 则让你“查得快”。
4.1 Histogram 与分位数计算
先说说 Histogram。它不像 Gauge 或 Counter 那样只存一个值,而是存了一组桶(bucket)。每个桶记录落在某个延迟区间内的请求数量。
比如 http_request_duration_seconds_bucket{le="0.1"} 表示延迟 ≤ 0.1 秒的请求数。桶越多,精度越高,但存储开销也越大。
4.1.1 从 Histogram 算分位数
PromQL 提供了 histogram_quantile() 函数。它的用法很简单:
# 计算过去 5 分钟的 P99 延迟
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
这里有个关键点:一定要配合 rate() 或 increase() 使用。为什么?因为 Histogram 存的是累积值,直接算分位数会得到从程序启动至今的累积分位数,那基本没有参考价值。
我的习惯:算分位数时,我几乎只用 rate()。它把桶的增量归一化到每秒,这样不同时间窗口的结果可比。如果你用 increase(),记得时间窗口要一致。
4.1.2 分位数的精度问题
你可能会问:P99 算出来准吗?说实话,取决于桶的分布。
Prometheus 的分位数计算是线性插值法。如果你的桶边界太稀疏,比如 1s 之后直接跳到 10s,那落在 1s 到 10s 之间的请求会被“平均”处理,结果自然不准。
我在项目中遇到过一个问题:P99 延迟算出来是 800ms,但实际压测发现有些请求跑了 3s。查了半天,发现是桶配置不合理——最大的桶只到 1s,超过 1s 的请求全被归到 le="+Inf" 里了。分位数算出来当然不准。
避坑指南:我曾经因为桶边界设置太宽,导致 P99 严重偏低。建议桶边界按指数增长,比如 0.005、0.01、0.025、0.05、0.1、0.25、0.5、1、2.5、5、10。这样既能覆盖大部分场景,又不会浪费太多存储。
4.1.3 聚合分位数
有时候你想看整个集群的 P99,而不是单个实例的。这时候需要先聚合桶,再算分位数:
# 先 sum 聚合所有实例的桶,再算 P99
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
注意 by (le) 不能少。因为 sum 会合并所有标签,但 le 必须保留,否则 PromQL 不知道桶的边界。
4.2 聚合操作:不只是 sum
很多人用 PromQL 聚合只会 sum 和 avg。其实还有几个非常实用的操作符。
| 操作符 | 作用 | 典型场景 |
|---|---|---|
sum |
求和 | 总 QPS、总错误数 |
avg |
平均值 | 平均延迟、平均 CPU 使用率 |
min / max |
最小值 / 最大值 | 找出最慢的实例、最低的可用性 |
count |
计数 | 有多少实例在运行、有多少实例超过阈值 |
stddev / stdvar |
标准差 / 方差 | 评估延迟波动、检测异常抖动 |
topk / bottomk |
前 K 个 / 后 K 个 | 找出最耗资源的 Top 10 实例 |
quantile |
聚合分位数(非 Histogram) | 对 Gauge 值直接算分位数 |
举个例子,你想找出 CPU 使用率最高的 5 个实例:
topk(5, avg by(instance) (rate(node_cpu_seconds_total{mode="user"}[5m])))
这里 topk 会返回 5 条时间序列,每条对应一个实例。注意 topk 是“瞬时向量”操作,不能直接用在范围向量上。
我的经验:用 topk 做告警要小心。因为 topk 的结果集会动态变化,今天排第一的实例明天可能掉出前五。如果你用它做告警,可能会漏掉持续高负载但没进前五的实例。我一般用 topk 做排查,用 阈值 + 聚合 做告警。
4.3 子查询:查得更细
子查询是 PromQL 里比较“高级”的功能。说白了,就是在一个查询里嵌套另一个查询,先算出一个中间结果,再对这个结果做二次计算。
什么时候需要子查询?举个例子:你想看过去 1 小时内,每 5 分钟的平均 CPU 使用率的最大值。普通查询做不到,因为 avg_over_time 只能算平均值,不能嵌套 max。
子查询的语法:
<原始查询>[<时间窗口>:<分辨率>]
比如:
# 先算每 5 分钟的平均 CPU,再取这 1 小时内的最大值
max_over_time(
avg by(instance) (rate(node_cpu_seconds_total{mode="user"}[5m]))
[1h:5m]
)
这里 [1h:5m] 表示:以 5 分钟为步长,计算过去 1 小时内的所有中间结果。然后 max_over_time 取这些结果的最大值。
性能警告:子查询非常消耗资源。因为它相当于在 Prometheus 内部执行了多次查询。我见过有人用子查询做实时告警,结果 Prometheus 直接 OOM。建议子查询只用于事后分析,或者配合 Recording Rules 使用。
4.4 Recording Rules:让查询飞起来
Recording Rules 是 Prometheus 里一个“低调但强大”的功能。它允许你把复杂的 PromQL 查询结果预先计算好,存成一条新的时间序列。以后查的时候,直接查这条新序列就行,不用再跑原始查询。
4.4.1 为什么需要 Recording Rules?
原因很简单:复杂查询太慢了。
比如你有一个 Dashboard,上面有 10 个 Panel,每个 Panel 都跑一次 histogram_quantile(0.99, rate(...))。每次刷新,Prometheus 都要重新计算一遍。如果数据量大,Dashboard 加载可能要等十几秒。
Recording Rules 的解决思路:提前算好,存起来。查询时直接读结果,毫秒级响应。
4.4.2 配置 Recording Rules
在 Prometheus 配置文件里加一个 rule_files 段,然后写规则文件:
# prometheus.yml
rule_files:
- "rules/*.yml"
规则文件内容:
# rules/recording.yml
groups:
- name: http_requests
interval: 30s
rules:
- record: job:http_request_duration_seconds:p99
expr: |
histogram_quantile(0.99,
sum by(le, job) (rate(http_request_duration_seconds_bucket[5m]))
)
- record: instance:http_request_duration_seconds:p50
expr: |
histogram_quantile(0.50,
sum by(le, instance) (rate(http_request_duration_seconds_bucket[5m]))
)
这里我定义了两条规则:
job:http_request_duration_seconds:p99:按 job 聚合的 P99 延迟instance:http_request_duration_seconds:p50:按 instance 聚合的 P50 延迟
规则名称的命名规范,我建议用 层级:指标名:聚合方式 的格式。这样一看就知道这条规则是干什么的。
我的习惯:Recording Rules 的刷新间隔不要设得太短。30s 到 1 分钟是比较合理的范围。太短了 Prometheus 计算压力大,太长了数据实时性不够。另外,规则名里带上 job 或 instance 前缀,方便在 Grafana 里搜索。
4.4.3 Recording Rules 的坑
Recording Rules 也不是银弹。我踩过几个坑:
- 存储膨胀:每条 Recording Rule 都会产生新的时间序列。如果你定义了几百条规则,存储量会翻倍。建议只对“高频查询”且“计算复杂”的指标做 Recording。
- 标签爆炸:如果原始查询的
by子句包含高基数标签(比如user_id),Recording Rule 会产生大量序列。我曾经因为一个by (user_id)的规则,导致 Prometheus 内存飙升。后来改成by (job)才解决。 - 规则更新:修改规则后,旧数据不会自动重算。你需要手动删除旧序列,或者等它们过期。我一般会在修改规则后,用
promtool检查一下语法,然后重启 Prometheus。
4.5 本章知识体系
下面这张图梳理了本章的核心逻辑。你可以看到,从原始指标到最终告警,每一步都有对应的 PromQL 技术。
4.6 实战:组合使用
最后,我分享一个我在生产环境里实际用过的组合查询。场景是:找出过去 10 分钟内,P99 延迟超过 1s 的 job,并列出对应的 Top 3 慢实例。
第一步:先算每个 job 的 P99
# 先定义 Recording Rule
- record: job:http_p99:5m
expr: |
histogram_quantile(0.99,
sum by(le, job) (rate(http_request_duration_seconds_bucket[5m]))
)
第二步:用子查询找出超过阈值的 job
# 过去 10 分钟内,P99 超过 1s 的 job
avg_over_time(job:http_p99:5m[10m]) > 1.0
第三步:对每个超阈值的 job,查它的 Top 3 慢实例
# 假设超阈值的 job 是 "api-gateway"
topk(3,
histogram_quantile(0.99,
sum by(le, instance) (
rate(http_request_duration_seconds_bucket{job="api-gateway"}[5m])
)
)
)
这样一套组合拳下来,你不仅能知道“哪个服务慢了”,还能知道“具体是哪几个实例拖了后腿”。
总结一下:Histogram 分位数让你看到“真实”的延迟分布;聚合操作帮你从全局视角看问题;子查询让你能做多层分析;Recording Rules 则让这一切变得“快”起来。这四个工具组合使用,才是生产级监控的正确姿势。