很多人把 PromQL 当成“画图 SQL”:会写 sum(rate(...)) 就能应付 80% 的面板,但一旦遇到多指标关联、实例集合变化、窗口语义、告警抖动,查询就开始失控。本文不绑定任何具体业务指标,而是从 Prometheus 的数据模型与 PromQL 的算子体系出发,系统梳理常用指令,并给出 Grafana 中可复用的查询范式。


1. 先建立数据模型:你在操作的不是“一个数”

Prometheus 存储的是时间序列(Time Series)。一条序列由两部分唯一确定:

  • 指标名(metric name):如 http_requests_totalnode_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]

规则记忆

  • 聚合算子(summax)和大部分二元运算,输入是 Instant Vector
  • *_over_timerateincrease 等函数,往往要求输入是 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_idtrace_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 bywithout

  • 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_totalnode_network_transmit_bytes_total

不要用 avg(counter)。正确姿势:

1
2
3
4
rate(http_requests_total[5m])       # 每秒平均增长率(最常用)
irate(http_requests_total[5m]) # 基于最后两个点,更敏感,适合告警
increase(http_requests_total[1h]) # 窗口内总增量
resets(http_requests_total[1h]) # 窗口内 reset 次数(进程重启、实例迁移)

rateirate 区别:

  • rate:对整个窗口线性回归,平滑,适合面板趋势
  • irate:瞬时斜率,对尖刺敏感,适合快速检测突变

窗口长度建议 ≥ 4× scrape_interval(如 15s 采集,窗口至少 [1m],常用 [5m])。

5.2 Gauge(可增可减)

典型:node_memory_MemAvailable_bytesprocess_resident_memory_bytes

可直接聚合,也可用窗口函数:

1
2
avg_over_time(node_load1[15m])
max_over_time(process_open_fds[1h])

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
2
metric > 0.9              # 过滤:只保留满足条件的序列,值不变
metric > bool 0.9 # 布尔:满足为 1,不满足为 0(或丢弃,视版本与上下文)

告警与“存在性判断”几乎总是 bool 模式。

7.3 集合运算

运算 含义
and 交集(两边都有且标签完全匹配)
or 并集
unless 差集:左边有、右边没有的序列

unless 是“实例从历史集合消失”类问题的核心算子,下文场景 6 会抽象演示。

7.4 匹配修饰符:on / ignoring

1
2
3
sum by (job) (rate(http_requests_total[5m]))
/
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
2
3
4
histogram_quantile(
0.99,
sum by (le, route) (rate(http_request_duration_seconds_bucket[5m]))
)

前提:bucket 边界合理、_bucket 是 Counter。不要对已经算好的 quantile 再 histogram_quantile

8.2 变化与趋势

1
2
3
4
changes(process_start_time_seconds[1h])   # 窗口内值变化次数
delta(temperature_celsius[1h]) # Gauge 窗口首尾差(慎用,非 Counter)
deriv(v[5m]) # 线性回归斜率
predict_linear(node_filesystem_free_bytes[1h], 4*3600) # 预测 N 秒后是否耗尽

8.3 存在性

1
2
absent(up{job="critical"} == 0)
absent_over_time(nonexistent_metric[5m])

absent 适合固定标签组合是否消失;动态实例集合变化更适合 unless 差集(见场景 6)。

8.4 标签变换

1
2
label_replace(up, "host", "$1", "instance", "(.*):.*")
label_join(up, "foo", ",", "job", "instance")

8.5 子查询(Subquery)

1
2
3
max_over_time(
rate(http_requests_total[5m])[1h:1m]
)

语法:[总窗口:步长],在总窗口内每隔步长对内部表达式求值,再外层聚合。用于“过去一小时每分钟的 5 分钟 rate 的最大值”这类嵌套时间维度问题。子查询成本高,能 recording rule 则预计算。

8.6 时间位移:offset@

1
2
node_memory_MemAvailable_bytes offset 1d    # 与昨天同一时刻对比
http_requests_total @ 1609746000 # 评估指定 Unix 时间戳

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 告警规则设计要点

  1. 条件表达式尽量稳定:用 rate[5m] 而非 irate[1m] 做主要告警(除非明确要捕尖刺)
  2. for 持续时间:过滤毛刺,如 for: 5m
  3. 多查询组合:Grafana 支持 A/B/C 查询,用 Math$A > 0 and $B > 0 做复合条件
  4. 注解模板:引用 $labels.instance$values.A 提升可行动性
  5. Recording Rule:把复杂、重复的子表达式下沉到 Prometheus 侧,告警只引用 recorded_metric

10. 六大通用场景与查询范式

以下全部使用社区常见指标名,可直接替换 label 适配你的环境。

场景 1:服务 QPS

1
sum by (job) (rate(http_requests_total[5m]))

场景 2:错误率(5xx 占比)

1
2
3
sum by (job) (rate(http_requests_total{code=~"5.."}[5m]))
/
sum by (job) (rate(http_requests_total[5m]))

场景 3:延迟 P99

1
2
3
4
histogram_quantile(
0.99,
sum by (le, job) (rate(http_request_duration_seconds_bucket[5m]))
)

场景 4:实例存活数(按 job 统计)

1
count by (job) (up == 1)

up 是 Prometheus 对被采集目标的健康探测,1 表示最近一次 scrape 成功。

场景 5:CPU 使用率(多核归一)

1
2
3
100 - (
avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100
)

场景 6:动态实例“消失”检测(集合差集范式)

需求抽象:历史上报过数据的 target,在最近短窗口内不再上报

probe_success 或任意表示“目标有采样”的 Gauge/Counter 为 M

Step A — 历史窗口内出现过的实例集合:

1
2
3
max by (instance, job) (
count_over_time(M{job=~"$job"}[30m] offset 10m) > bool 0
)

Step B — 当前短窗口仍活跃的实例集合:

1
2
3
max by (instance, job) (
count_over_time(M{job=~"$job"}[5m]) > bool 0
)

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 > Nfor: 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. 学习路径建议

  1. 先熟练:选择器 → ratesum by → 错误率除法 → histogram_quantile
  2. 再攻克on/ignoringunless*_over_timebool 修饰符
  3. 最后:子查询、recording rule、告警与 Grafana 变量联动

PromQL 的难点从来不是背函数表,而是把运维问题翻译成:什么维度、什么时间窗、什么集合关系。掌握这套建模方法后,无论指标名如何变化,你都能快速写出正确、可维护、可告警的查询。


延伸阅读:Prometheus 官方 QueryingOperators;Grafana Alerting 文档。