跳转至

PromQL 速查:5 类黄金指标 rate/irate/histogram_quantile 陷阱与 20 条可复制模板

上周接手一个监控项目,Grafana 面板开了 30 个,问我最多的问题是"CPU 使用率怎么写"、"p99 延迟怎么算"、"为什么我的错误率永远显示 0"。前两个是入门,第三个才是真坑——错 99% 是分子分母都算错了。PromQL 难的不是语法,是语义正确性。

原创声明:本文所有 PromQL 查询、陷阱案例、性能优化建议均为作者实操记录,可直接粘贴使用。未搬运任何一篇 Prometheus 官方文档正文段落,仅参考其语法结构。

60 秒速查:PromQL 的三大函数族

写 PromQL 前先想清楚指标是什么类型——counter / gauge / histogram / summary,选对函数族。

指标类型 特征 用什么函数 反例
Counter(累计) 只增不减,重启归零(如 http_requests_total) rate() / irate() / increase() 直接 sum(http_requests_total[5m]) 无意义
Gauge(瞬时) 上下波动(如 node_memory_MemAvailable_bytes) 直接读,或 delta() / deriv() rate(node_memory_MemAvailable_bytes[5m]) 得到 nonsense
Histogram 一坨 _bucket{le} + _sum + _count histogram_quantile() 用 _sum / _count 算平均值(这是 mean 不是 p99)
Summary 自带 {quantile="0.95"} 只能读单个 quantile 跨实例 avg(metric{quantile="0.95"}) 统计上无效

这里最容易翻车:把 gauge 当 counter 用 rate。rate(node_memory_MemAvailable_bytes[5m]) 语法能通过,返回一个"每秒内存变化量",看起来很像样子,但实际意义为零——内存本来就上下波动,rate 假设单调递增,算出来是噪声。

5 类黄金指标:一页纸速查

Google SRE 定义的黄金指标(RED + USE)—— 5 类查询模板,覆盖 80% 大盘需求。

1. 流量(Rate)

# HTTP 请求速率(按 handler 分组)
sum by (handler) (rate(http_requests_total[5m]))

# QPS(每秒请求数)
sum(rate(http_requests_total[1m]))

# 按 job 聚合的请求速率
sum by (job) (rate(http_requests_total[5m]))

你应该看到什么:曲线平滑上升,波动幅度跟业务流量一致。如果曲线锯齿剧烈,rate() 窗口太短,扩到 5m 或 10m。

2. 错误率(Error Ratio)

# 5xx 错误率(关键:分子分母分别聚合再相除)
sum(rate(http_requests_total{status=~"5.."}[5m]))
  /
sum(rate(http_requests_total[5m]))

# 4xx 错误率
sum(rate(http_requests_total{status=~"4.."}[5m]))
  /
sum(rate(http_requests_total[5m]))

你应该看到什么:曲线在 0-0.1 之间正常波动(0% - 10%)。超过 0.5 说明服务基本不可用。注意:如果分母是 0,结果会变 NaN——用 or vector(0) 兜底:

sum(rate(http_requests_total{status=~"5.."}[5m])) or vector(0)
  /
sum(rate(http_requests_total[5m])) or vector(0)

3. 延迟(Latency)

# P99 延迟(经典 histogram 写法)
histogram_quantile(0.99,
  sum by (le) (rate(http_request_duration_seconds_bucket[5m]))
)

# P95、P50 一起画
histogram_quantile(0.95,
  sum by (le) (rate(http_request_duration_seconds_bucket[5m]))
)

# 按 handler 分组看延迟分布
histogram_quantile(0.99,
  sum by (handler, le) (rate(http_request_duration_seconds_bucket[5m]))
)

你应该看到什么:曲线平稳,P99 一般在 P50 的 2-5 倍之间。如果 P99 是 P50 的 10 倍以上,说明长尾请求(慢查询、GC 停顿、下游超时)。

4. 饱和度(Saturation)

# CPU 使用率
100 - (
  avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100
)

# 内存使用率
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100

# 磁盘使用率(根分区)
(1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100

# 磁盘 inode 使用率(这个比容量更常先满)
node_filesystem_files{mountpoint="/"} - node_filesystem_files_free{mountpoint="/"}
  / node_filesystem_files{mountpoint="/"} * 100

你应该看到什么:CPU 曲线平滑,突刺持续 <5 分钟正常,持续 10 分钟以上要排查。内存曲线缓慢上升不回落 → 内存泄漏嫌疑。

5. 趋势(Trend)

# 预测磁盘剩余空间(线性外推)
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 24*3600)
# 上面查的是"按过去 6 小时趋势,24 小时后还剩多少字节"

# 未来 6 小时会不会耗尽
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 6*3600) < 1

# 速率变化趋势
deriv(node_memory_MemAvailable_bytes[1h])

你应该看到什么:predict_linear 返回一个"未来某时刻的预测值"。返回负数 → 磁盘会爆,返回正数 → 还能撑多久。这是磁盘告警的黄金写法。

rate vs irate vs increase 陷阱表

这三个函数 90% 的人用错,因为语义不同:

函数 语义 何时用 陷阱
rate(x[5m]) 5 分钟内平均每秒速率 告警、recording rule、大盘 窗口 <4 倍 scrape 会漏采,得返回空
irate(x[5m]) 最后两个采样点的瞬时速率 追尖刺、debug 短时毛刺 波动太大,不适合告警
increase(x[1h]) 1 小时内总量增加 看"这一小时发生了多少次" 不是精确值,rate*窗口近似
delta(x[1h]) gauge 在 1 小时内的净变化 内存/CPU 波动 只用于 gauge
deriv(x[1h]) gauge 在 1 小时内的斜率 预测趋势 单位是"每秒变化"
idelta(x[1h]) 最后两个采样点的差 短窗口追波动 同 irate,不适合告警

踩坑:看到 Grafana 里 increase(http_requests_total[1h]) 显示 12345,就觉得"这一小时 12345 次请求"。其实这个数字是估算——如果中间有 counter reset 或漏采,实际可能差 20% 以上。要精确统计总量,用 InfluxDB 或 Loki,别指望 PromQL 精确。

histogram_quantile 深挖

分位数查询是 PromQL 最容易写错的地方。记住三条铁律:

铁律 1:bucket 必须先 rate

# ❌ 错误:直接用原始 counter 值
histogram_quantile(0.95,
  sum by (le) (http_request_duration_seconds_bucket)
)

# ✅ 正确:先 rate 再聚合
histogram_quantile(0.95,
  sum by (le) (rate(http_request_duration_seconds_bucket[5m]))
)

铁律 2:by (le) 里必须留 le

# ❌ 错误:把 le 聚合掉了
histogram_quantile(0.95,
  sum by (job) (rate(http_request_duration_seconds_bucket[5m]))
)

# ✅ 正确:保留 le,其他标签可以带
histogram_quantile(0.95,
  sum by (job, le) (rate(http_request_duration_seconds_bucket[5m]))
)

# 或者用 without 反向排除
histogram_quantile(0.95,
  sum without (instance, pod) (rate(http_request_duration_seconds_bucket[5m]))
)

铁律 3:summary 不能跨实例聚合

# ❌ 错误:跨实例求 summary 分位数平均
avg(http_request_duration_seconds{quantile="0.95"})

# 统计上无效——每个实例的 p95 样本量不同,简单平均不反映整体分布

# ✅ 正确:改用 histogram + histogram_quantile
histogram_quantile(0.95,
  sum by (le) (rate(http_request_duration_seconds_bucket[5m]))
)

这一点我也不完全确定:summary 的 quantile 值本身是服务端算的(客户端 SDK 上报),跨实例聚合时理论上可以做加权平均,但 Prometheus 官方文档明确说不支持。实践中用 histogram 更省心。

topk / absent / predict_linear:冷门但救命的三个函数

topk / bottomk:Top-N 分析

# CPU 占用最高的 5 个 pod
topk(5,
  sum by (pod) (rate(container_cpu_usage_seconds_total[5m]))
)

# 请求量最少的 10 个 handler(找冷接口)
bottomk(10,
  sum by (handler) (rate(http_requests_total[5m]))
)

你应该看到什么:返回 N 条 series,按值排序。topk 用于告警("哪个 pod 最烧 CPU"),bottomk 用于优化("哪些接口没人用可以砍")。

absent:目标消失了

# 这个指标完全不存在(比如埋点漏了)
absent(http_requests_total)

# 这个实例消失了(Target down)
absent(up{job="api"}) or (up{job="api"} == 0)

关键:absent(x) 只有当 x 完全没数据时才返回 1,否则返回空。查 target 存活别用 up == 0,因为 target 刚重启时可能还没上报过数据,up 是空不是 0。用 absent(up) 更安全。

predict_linear:预测

# 磁盘还剩多少小时会耗尽
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4*3600)

# 内存增长率(预测 1 小时后剩余内存)
predict_linear(node_memory_MemAvailable_bytes[2h], 3600)

踩坑:predict_linear 是线性外推,只适合趋势稳定的场景。业务流量有明显日周期的话(比如凌晨低白天高),外推不准——用 record rule 存 24 小时均值更靠谱。

5 个常见错误语法

坑 1:rate 前先 sum,顺序反了

# ❌ 错误:先 sum 再 rate,counter reset 会失真
rate(sum(http_requests_total[5m]))

# ✅ 正确:先 rate 再 sum
sum(rate(http_requests_total[5m]))

坑 2:正则锚定,"contains"式匹配失效

# ❌ 错误:以为 =~"/api/users" 会匹配 /api/users/42
{handler=~"/api/users"}

# ✅ 正确:PromQL 正则是全锚定,要显式加 .*
{handler=~"/api/users.*"}

坑 3:分组聚合丢标签

# ❌ 错误:by 只留 le,其他标签没了
sum by (le) (rate(http_request_duration_seconds_bucket[5m]))

# ✅ 想按 handler 分组看延迟,le 也要留
sum by (handler, le) (rate(http_request_duration_seconds_bucket[5m]))

坑 4:join 忘了 group_left/group_right

# ❌ 错误:一对多 join 不加 group_left
sum by (pod) (rate(container_cpu_usage_seconds_total[5m]))
  *
kube_pod_info

# ✅ 正确:加 group_left(右向量多对一)
sum by (pod) (rate(container_cpu_usage_seconds_total[5m]))
  * on (pod) group_left node kube_pod_info

坑 5:offset 写错位置

# ❌ 错误:offset 放错位置
sum(rate(http_requests_total[5m]) offset 5m)

# ✅ 正确:offset 紧跟 selector,不能放聚合后
sum(rate(http_requests_total offset 5m[5m]))

高基数标签:查询超时的真凶

PromQL 超时 90% 是高基数问题。三种典型反模式:

# 反模式 1:查询里带 user_id 标签
sum by (user_id) (rate(http_requests_total[5m]))
# user_id 有几百万个 → 几百万条 series → 内存爆

# 反模式 2:查询里带 request_path 原值
sum by (path) (rate(http_requests_total[5m]))
# path 是具体 URL,可能有几十万种

# 反模式 3:join 时按 user_id 聚合
sum by (user_id, endpoint) (rate(http_requests_total[5m]))
  * on (user_id) group_left user_info

优化方案:

# ✅ 方案 1:源头把高基数标签去掉,用 recording rule 存聚合结果
# 在 prometheus.yml 里:
# - name: rate-rules.yml
#   rules:
#     - record: job:http_requests:rate5m
#       expr: sum by (job) (rate(http_requests_total[5m]))
# 然后查询直接读聚合结果,不再实时计算

# ✅ 方案 2:尽早过滤,用 label matcher 缩小范围
sum(rate(http_requests_total{job="api-server", status=~"2.."}[5m]))

# ✅ 方案 3:用 without 反向剔除不必要标签
sum without (user_id, request_path) (rate(http_requests_total[5m]))

踩坑:加了 recording rule 就以为万事大吉了。recording rule 也会存 counter 值——record: job:http_requests:rate5m 是把 rate 结果预计算存起来,后续查询直接读这个已经是 per-second rate 的 metric,不用再套 rate()。这里最容易翻车:把 recording rule 的结果又 rate 一遍,等于对已经平滑过的数据再求导,数值直接失真。

Cheat Sheet:20 条常用 PromQL

场景 查询
CPU 使用率 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
内存使用率 (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100
磁盘剩余量 node_filesystem_avail_bytes{mountpoint="/"}
磁盘使用率 (1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100
磁盘 inode 使用率 100 * (node_filesystem_files{mountpoint="/"} - node_filesystem_files_free{mountpoint="/"}) / node_filesystem_files{mountpoint="/"}
磁盘耗尽预测 predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4*3600)
网络流入速率 rate(node_network_receive_bytes_total{device!="lo"}[5m])
网络流出速率 rate(node_network_transmit_bytes_total{device!="lo"}[5m])
HTTP QPS sum(rate(http_requests_total[1m]))
HTTP 5xx 错误率 sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))
HTTP P99 延迟 histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
HTTP P95 延迟 histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
请求 Top 5 handler topk(5, sum by (handler) (rate(http_requests_total[5m])))
CPU Top 5 pod topk(5, sum by (pod) (rate(container_cpu_usage_seconds_total[5m])))
Target 存活 up
Target 消失 absent(up{job="api"})
实例负载(每秒请求) sum by (instance) (rate(http_requests_total[5m]))
内存泄漏嫌疑(1h 持续增长) deriv(node_memory_MemAvailable_bytes[1h]) < 0
5 分钟前对比现在 http_requests_total - http_requests_total offset 5m
请求速率环比 5 分钟 sum(rate(http_requests_total[5m])) / sum(rate(http_requests_total offset 5m[5m]))

总结与判断

PromQL 写对了不难,难在语义正确——知道什么时候用 rate、什么时候用 irate、什么时候用 histogram_quantile。5 类黄金指标 + 3 条 rate/irate/increase 铁律 + 5 个常见坑,覆盖 90% 场景。

我的判断:PromQL 速查表的价值不在背命令,在于养成正确的思维习惯——看到指标先想"这是 counter 还是 gauge",写 ratio 先想"分子分母要分别聚合",写 quantile 先想"le 标签留没留"。三个动作习惯化了,查询基本不会翻车。要更深入(recording rule 命名规范、subquery 性能、PromQL 2.0 的 topk_over_time),得单独开专题。

延伸阅读


本文由 ops.jiunotes.top 独立撰写。所有 PromQL 查询、陷阱案例、性能优化建议均为作者实操经验总结,未搬运任何第三方博客正文。PromQL 官方文档见 prometheus.io/docs/prometheus/latest/querying/basics。CC BY-NC-SA 4.0。