PromQL 深度实战:语法体系、常用算子与 Grafana 场景化查询
很多人把 PromQL 当成“画图 SQL”:会写 sum(rate(...)) 就能应付 80% 的面板,但一旦遇到多指标关联、实例集合变化、窗口语义、告警抖动,查询就开始失控。本文不绑定任何具体业务指标,而是从 Prometheus 的数据模型与 PromQL 的算子体系出发,系统梳理常用指令,并给出 Grafana 中可复用的查询范式。
1. 先建立数据模型:你在操作的不是“一个数”
Prometheus 存储的是时间序列(Time Series)。一条序列由两部分唯一确定:
- 指标名(metric name):如
http_requests_total、node_cpu_seconds_total - 标签集(labels):如
{job="api", instance="10.0.0.1:8080", method="GET", code="200"}
每个时间点上的采样是 (timestamp, value)。因此 PromQL 的输入/输出从来不是“单个 float”,而是向量(vector)——一组带标签的序列集合。
理解这一点后,写查询的第一问永远是:
我要保留哪些 label?要消掉哪些 label?窗口内统计什么?
2. 两种向量:Instant Vector 与 Range Vector
这是 PromQL 最容易混淆、也最关键的概念。
Instant Vector(瞬时向量)
在某个时刻 t,每个元素是一条序列的最新样本值(在查询 lookback 窗口内取最新点,默认 5m,可用 @ 指定评估时刻)。
1 | http_requests_total{job="api"} |
Range Vector(区间向量)
在某个时刻 t,每个元素是该序列在 [t-窗口, t] 内的一串样本点。
1 | http_requests_total{job="api"}[5m] |
规则记忆:
- 聚合算子(
sum、max)和大部分二元运算,输入是 Instant Vector *_over_time、rate、increase等函数,往往要求输入是 Range Vector(带[窗口])
写错向量类型,Prometheus 会直接报错,或得到语义完全不对的结果。
3. 选择器:如何精确圈定序列集合
3.1 指标选择器
1 | metric_name{label_matcher,...} |
3.2 标签匹配器(Matchers)
| 写法 | 含义 |
|---|---|
label="foo" |
精确等于 |
label!="foo" |
不等于 |
| `label=~”a | b”` |
| `label!~”a | b”` |
1 | http_requests_total{job=~"api|gateway", code!~"2.."} |
3.3 空选择器与 {__name__=...}
up等价于{__name__="up"}- 可通过
{__name__=~"http_.*"}按名称模式批量选取
3.4 基数(Cardinality)意识
每多一个高基数字段(如 user_id、trace_id),序列数量就指数级膨胀。PromQL 写得“对”但慢到不可用,通常是标签维度没收敛。生产查询应尽早 sum by (必要维度) 或 recording rule 预聚合。
4. 聚合:把多条序列合并成更少条
4.1 基础聚合算子
对同一时刻的 Instant Vector 做聚合:
| 算子 | 作用 |
|---|---|
sum |
求和 |
avg |
平均 |
min / max |
最小/最大 |
count |
计数(有多少条输入序列) |
stddev / stdvar |
标准差/方差 |
group |
保留分组内所有 label 的并集(较少直接用) |
1 | sum by (job) (rate(http_requests_total[5m])) |
4.2 by 与 without
sum by (a, b):只保留列出的 label,其余参与聚合的 label 被丢弃sum without (instance):去掉列出的 label 后,对其余 label 分组聚合
二者等价关系:sum without (x) ≈ 按“除 x 外所有 label”分组。选哪个取决于你最终想保留什么维度。
4.3 topk / bottomk
1 | topk(5, sum by (instance) (rate(node_network_receive_bytes_total[5m]))) |
取聚合后值最大的 5 条序列。注意:topk 在告警里可能造成“榜单抖动”,需配合 for 或 recording rule。
4.4 count_values
1 | count_values("version", build_info) |
统计某个 label 取值的分布(如各版本实例数量)。
4.5 quantile(聚合分位,非 histogram)
1 | quantile(0.95, sum by (le) (...)) # 与 histogram_quantile 不同,用于多条序列间的分位 |
5. Counter 与 Gauge:决定你用哪类函数
5.1 Counter(只增不减,可 reset)
典型:http_requests_total、node_network_transmit_bytes_total
不要用 avg(counter)。正确姿势:
1 | rate(http_requests_total[5m]) # 每秒平均增长率(最常用) |
rate 与 irate 区别:
rate:对整个窗口线性回归,平滑,适合面板趋势irate:瞬时斜率,对尖刺敏感,适合快速检测突变
窗口长度建议 ≥ 4× scrape_interval(如 15s 采集,窗口至少 [1m],常用 [5m])。
5.2 Gauge(可增可减)
典型:node_memory_MemAvailable_bytes、process_resident_memory_bytes
可直接聚合,也可用窗口函数:
1 | avg_over_time(node_load1[15m]) |
6. 区间函数 *_over_time:在窗口内对单条序列统计
输入必须是 Range Vector [窗口]:
| 函数 | 含义 |
|---|---|
avg_over_time |
窗口内平均值 |
max_over_time / min_over_time |
最大/最小 |
sum_over_time |
求和 |
count_over_time |
样本点个数(不是值的和) |
stddev_over_time / stdvar_over_time |
标准差/方差 |
quantile_over_time(φ, v) |
窗口内分位数 |
last_over_time |
最后一个样本值 |
present_over_time |
窗口内是否出现过样本(有则 1) |
count_over_time 的典型用法:判断“某段时间内是否有过数据”
1 | count_over_time(up{job="node"}[10m]) > bool 0 |
注意:这里必须用 > bool 0,否则 > 0 只是过滤,返回值仍是原始样本个数,图表会像一条下降曲线——这是生产里极高频的坑。
7. 二元运算与向量匹配:PromQL 的“关系代数”
7.1 算术:+ - * / % ^
两边 Instant Vector 按标签匹配后逐点运算。标签不一致的序列默认不参与运算(除非用匹配修饰符)。
7.2 比较:== != < > <= >=
两种模式:
1 | metric > 0.9 # 过滤:只保留满足条件的序列,值不变 |
告警与“存在性判断”几乎总是 bool 模式。
7.3 集合运算
| 运算 | 含义 |
|---|---|
and |
交集(两边都有且标签完全匹配) |
or |
并集 |
unless |
差集:左边有、右边没有的序列 |
unless 是“实例从历史集合消失”类问题的核心算子,下文场景 6 会抽象演示。
7.4 匹配修饰符:on / ignoring
1 | sum by (job) (rate(http_requests_total[5m])) |
若两边 label 不完全一致,需要显式声明按哪些 label 对齐:
1 | A / on(job, env) group_left B |
on(...):只按列出的 label 匹配ignoring(...):忽略列出的 label,其余参与匹配group_left/group_right:多对一匹配时,把“多”的一侧的额外 label 带到结果里
这是写错误率、饱和度、配额使用率时必会的技能。
8. 其他高频函数
8.1 Histogram 分位
1 | histogram_quantile( |
前提:bucket 边界合理、_bucket 是 Counter。不要对已经算好的 quantile 再 histogram_quantile。
8.2 变化与趋势
1 | changes(process_start_time_seconds[1h]) # 窗口内值变化次数 |
8.3 存在性
1 | absent(up{job="critical"} == 0) |
absent 适合固定标签组合是否消失;动态实例集合变化更适合 unless 差集(见场景 6)。
8.4 标签变换
1 | label_replace(up, "host", "$1", "instance", "(.*):.*") |
8.5 子查询(Subquery)
1 | max_over_time( |
语法:[总窗口:步长],在总窗口内每隔步长对内部表达式求值,再外层聚合。用于“过去一小时每分钟的 5 分钟 rate 的最大值”这类嵌套时间维度问题。子查询成本高,能 recording rule 则预计算。
8.6 时间位移:offset 与 @
1 | node_memory_MemAvailable_bytes offset 1d # 与昨天同一时刻对比 |
9. Grafana 中的 PromQL:场景不等于“把查询贴进去”
Grafana 是可视化与告警编排层,PromQL 是计算层。同一表达式在不同查询类型下语义不同。
9.1 Range Query vs Instant Query
| 类型 | Grafana 场景 | 行为 |
|---|---|---|
| Range | Time series 面板、趋势图 | 在时间轴上每个 step 求值,返回多条曲线 |
| Instant | Table、Stat、告警条件、当前快照 | 只在一个时间点求值 |
排查“此刻谁异常”,务必用 Instant;看趋势用 Range。
9.2 变量(Variables)
1 | sum(rate(http_requests_total{job=~"$job", env="$env"}[5m])) by (job) |
$job多选时通常配合=~"$job"(正则)- 变量默认值、All 选项会影响正则,注意空匹配与
.*的性能
9.3 面板类型与查询分工
- Time series:趋势、同比(
offset) - Stat / Gauge:当前值、SLO 达标率
- Table + Instant:实例清单、TopN 排行
- Heatmap:配合 histogram bucket
9.4 告警规则设计要点
- 条件表达式尽量稳定:用
rate[5m]而非irate[1m]做主要告警(除非明确要捕尖刺) for持续时间:过滤毛刺,如for: 5m- 多查询组合:Grafana 支持 A/B/C 查询,用
Math或$A > 0 and $B > 0做复合条件 - 注解模板:引用
$labels.instance、$values.A提升可行动性 - Recording Rule:把复杂、重复的子表达式下沉到 Prometheus 侧,告警只引用
recorded_metric
10. 六大通用场景与查询范式
以下全部使用社区常见指标名,可直接替换 label 适配你的环境。
场景 1:服务 QPS
1 | sum by (job) (rate(http_requests_total[5m])) |
场景 2:错误率(5xx 占比)
1 | sum by (job) (rate(http_requests_total{code=~"5.."}[5m])) |
场景 3:延迟 P99
1 | histogram_quantile( |
场景 4:实例存活数(按 job 统计)
1 | count by (job) (up == 1) |
up 是 Prometheus 对被采集目标的健康探测,1 表示最近一次 scrape 成功。
场景 5:CPU 使用率(多核归一)
1 | 100 - ( |
场景 6:动态实例“消失”检测(集合差集范式)
需求抽象:历史上报过数据的 target,在最近短窗口内不再上报。
设 probe_success 或任意表示“目标有采样”的 Gauge/Counter 为 M:
Step A — 历史窗口内出现过的实例集合:
1 | max by (instance, job) ( |
Step B — 当前短窗口仍活跃的实例集合:
1 | max by (instance, job) ( |
Step C — 差集(消失清单):
1 | A unless on (instance, job) B |
Step D — 按 job 统计消失数量(告警阈值):
1 | count by (job) (A unless on (instance, job) B) |
参数调优逻辑(与业务无关的通用原则):
- 当前窗口(如
[5m]):应 ≥ 3× scrape_interval,表示“真的断了” - 历史窗口 + offset(如
[30m] offset 10m):避免把刚上线的实例误判为消失;offset 形成“隔离带” - 告警:
count > N且for: 10m,防止批量重启/网络抖动误报 - 展示:清单用 Table + Instant;数量趋势用 Time series
若担心采集系统整体故障导致全体 M 归零,可叠加“采集器自身健康”条件,例如 up{job="prometheus"} 或独立 heartbeat 指标,避免“全灭却无人知晓”。
11. 性能、正确性与反模式清单
| 反模式 | 后果 | 建议 |
|---|---|---|
| 高基数 label 不做聚合 | 查询超时、OOM | 尽早 sum by (低基数维度) |
Counter 直接 avg |
语义错误 | 先 rate/increase |
用 > 0 代替 > bool 0 做存在性 |
图表值怪异 | 显式 bool |
告警用极短 irate + 无 for |
告警风暴 | 平滑 + 持续条件 |
| 复杂表达式重复出现在 20+ 面板 | 每次全量计算 | Recording rule |
| 子查询嵌套过深 | 评估极慢 | 预聚合或缩短窗口 |
12. 学习路径建议
- 先熟练:选择器 →
rate→sum by→ 错误率除法 →histogram_quantile - 再攻克:
on/ignoring、unless、*_over_time、bool修饰符 - 最后:子查询、recording rule、告警与 Grafana 变量联动
PromQL 的难点从来不是背函数表,而是把运维问题翻译成:什么维度、什么时间窗、什么集合关系。掌握这套建模方法后,无论指标名如何变化,你都能快速写出正确、可维护、可告警的查询。
延伸阅读:Prometheus 官方 Querying 与 Operators;Grafana Alerting 文档。





