PromQL 基础
PromQL(Prometheus Query Language)是 Prometheus 的查询语言。它和 SQL 完全不同——SQL 查的是「表里的行」,PromQL 查的是「时间轴上的序列」。
这一章只讲最基础的一层:怎么把想要的序列挑出来。运算符和函数留到后两章。听起来很简单,但恰恰是这一层的两个概念——即时向量 vs 区间向量——是 90% 新手困惑的根源。
1. PromQL 的四种数据类型
先建立全局认知。PromQL 的每个表达式都会求值成以下四种类型之一:
| 类型 | 英文 | 是什么 | 例子 |
|---|---|---|---|
| 即时向量 | instant vector | 一组序列,每条序列在同一时刻有一个样本 | up |
| 区间向量 | range vector | 一组序列,每条序列有一段时间窗口内的多个样本 | up[5m] |
| 标量 | scalar | 一个单独的浮点数 | 3.14 |
| 字符串 | string | 一个字符串(几乎用不到) | "foo" |
每个函数都对输入类型有严格要求,弄错就报错:
rate(http_requests_total) # ❌ rate 要区间向量,给了即时向量
sum(http_requests_total[5m]) # ❌ sum 要即时向量,给了区间向量绝大多数「表达式解析错误」都源于此。养成习惯:写每个函数前先问自己「它吃什么类型?」
2. 即时向量:某一刻的快照
2.1 最简单的查询
在 Prometheus Web UI 的查询框里输入一个指标名,就是一个即时向量查询:
up结果类似:
up{job="prometheus", instance="localhost:9090"} 1
up{job="node", instance="10.0.1.5:9100"} 1
up{job="node", instance="10.0.1.6:9100"} 0
up{job="api", instance="10.0.2.11:8080"} 1这就是即时向量:N 条序列,每条序列在「查询时刻」有一个值。
up 是 Prometheus 自动生成的指标——每次抓取一个目标,它就写一个 up 样本:抓取成功为 1,失败为 0。这是最常用的健康检查指标:
# 找出所有挂掉的目标
up == 02.2 「查询时刻」到底取哪个样本
这里有个细节值得讲清楚。假设你在 10:00:07 执行 up,但抓取间隔是 15 秒,样本落在 10:00:00 和 10:00:15。这时返回什么?
Prometheus 的规则是:向前回溯,取查询时刻之前最近的一个样本。
样本: 10:00:00 (1) 10:00:15 (1)
│ │
查询时刻: │ 10:00:07 │
└──────────┘
返回 10:00:00 的样本,但时间戳标记为 10:00:07回溯有个上限,叫 lookback delta,默认 5 分钟:
如果查询时刻往前 5 分钟内都没有样本 → 这条序列不出现在结果里影响一:目标下线后,图表还会「拖」5 分钟。 一个实例挂了,抓取失败,不再有新样本。但接下来 5 分钟内查询它仍会返回最后一个值(一条水平直线),5 分钟后才彻底消失。
(例外:如果是「序列在抓取结果中消失」而非「抓取失败」,Prometheus 会写入 staleness marker,线会立刻断掉。详见第 4 章。)
影响二:抓取间隔不能超过 5 分钟。
如果 scrape_interval: 10m,那么每次抓取后的第 5 分钟到第 10 分钟之间,查询会返回空——你的监控图会变成虚线。抓取间隔请保持在 1 分钟以内,除非你同时调大了 --query.lookback-delta。
3. 区间向量:一段时间的样本序列
3.1 语法与含义
在选择器后面加 [时长],就得到区间向量:
http_requests_total[5m]它返回的是每条序列在最近 5 分钟内的所有样本。假设抓取间隔 15 秒,5 分钟就是约 20 个样本:
http_requests_total{job="api", instance="10.0.2.11:8080"} =>
1203441 @1714540800
1203987 @1714540815
1204512 @1714540830
... (共约 20 个样本)对比一下,即时向量只有一个值:
http_requests_total{job="api", instance="10.0.2.11:8080"} 12045123.2 时长单位
ms 毫秒
s 秒
m 分钟
h 小时
d 天 (= 24h,不考虑夏令时)
w 周 (= 7d)
y 年 (= 365d,不考虑闰年)可以组合书写,但必须从大到小、不能重复:
http_requests_total[1h30m] # ✅ 1 小时 30 分
http_requests_total[1d12h] # ✅ 1 天 12 小时
http_requests_total[30m1h] # ❌ 顺序错了
http_requests_total[1h1h] # ❌ 单位重复3.3 区间向量不能直接画图
这是新手最容易撞的墙:
http_requests_total[5m]在 Web UI 的 Table 标签页能看到一堆样本,但切到 Graph 标签页会报错:
Error executing query: invalid expression type "range vector" for range query,
must be Scalar or instant Vector为什么? 因为画图需要「每个时间点一个值」,而区间向量每个时间点是「一堆值」。你必须先用函数把它压缩成一个值:
rate(http_requests_total[5m]) # 区间 → 速率(一个值)
avg_over_time(node_load1[5m]) # 区间 → 平均值(一个值)
max_over_time(go_goroutines[1h]) # 区间 → 最大值(一个值)
count_over_time(up[1h]) # 区间 → 样本个数记住这条规则就不会错:
区间向量永远不会单独出现在最终结果里,它一定被某个函数包裹着。
反过来说,看到 [5m] 就应该去找它外面的那个函数。找不到?那这个查询画不出图。
3.4 区间长度怎么选
这是个需要判断力的问题。核心约束:
区间长度必须至少能容纳 2 个样本,否则函数返回空。
scrape_interval = 15s
rate(x[15s]) → 区间内可能只有 1 个样本 → 返回空 ❌
rate(x[1m]) → 约 4 个样本 → 可以,但抖动大
rate(x[5m]) → 约 20 个样本 → ✅ 推荐经验法则:区间长度 ≥ 抓取间隔 × 4。
| 抓取间隔 | 建议最小区间 | 常用值 |
|---|---|---|
| 15s | 1m | [5m] |
| 30s | 2m | [5m] |
| 1m | 4m | [10m] |
区间越长越平滑(抹平毛刺),越短越灵敏(能看到突刺)。告警一般用 [5m],看趋势大盘可以用 [30m] 甚至 [1h]。
面板里硬编码 [5m] 有个问题:当你把时间范围拉到「最近 30 天」时,Grafana 的步长会变成几十分钟,而 [5m] 的窗口小于步长,会跳过大量数据点,图表出现锯齿甚至空洞。
Grafana 提供了两个内置变量:
rate(http_requests_total[$__rate_interval]) ← 推荐,专为 rate 设计
rate(http_requests_total[$__interval]) ← 等于当前步长,可能太小$__rate_interval 会自动取 max(4 × 抓取间隔, 步长 + 抓取间隔),保证任何时间范围下都有足够样本。在 Grafana 里写 rate,优先用它。
4. 选择器与匹配器
4.1 基本形式
一个完整的即时向量选择器长这样:
http_requests_total{job="api", status="200"}拆开看:
http_requests_total { job="api" , status="200" }
│ │ │
指标名 匹配器 1 匹配器 2多个匹配器之间是 AND 关系——必须全部满足才会被选中。
4.2 四种匹配器
| 匹配器 | 名称 | 含义 |
|---|---|---|
= | 相等 | 标签值精确等于给定字符串 |
!= | 不等 | 标签值不等于给定字符串 |
=~ | 正则匹配 | 标签值匹配给定正则 |
!~ | 正则不匹配 | 标签值不匹配给定正则 |
逐个看例子:
# = 精确匹配
http_requests_total{status="200"}
# != 排除
http_requests_total{status!="200"}
# =~ 正则:所有 5xx
http_requests_total{status=~"5.."}
# !~ 正则排除:排除所有健康检查路径
http_requests_total{path!~"/health.*"}
# 多条件组合(AND)
http_requests_total{job="api", method="POST", status=~"5..", region!="test"}4.3 正则匹配的三个坑
坑一:正则是「完全匹配」的。
Prometheus 使用 RE2 正则,并且会自动在两端加上 ^ 和 $:
# ❌ 匹配不到 500、503
http_requests_total{status=~"5"}
# 因为它等价于 status=~"^5$",要求整个值就是 "5"
# ✅ 正确写法
http_requests_total{status=~"5.."} # 5 开头的三位数
http_requests_total{status=~"5.*"} # 5 开头的任意长度这是新手最常见的困惑,没有之一。
坑二:| 表示「或」,用来枚举多个值。
# 匹配 GET 或 POST
http_requests_total{method=~"GET|POST"}
# 匹配多个 job
up{job=~"api|web|worker"}
# 等价的排除写法
up{job!~"test|staging"}坑三:空值匹配有特殊语义。
# 匹配「没有 env 标签」或「env 标签为空」的序列
http_requests_total{env=""}
# 匹配「有 env 标签且非空」的序列
http_requests_total{env!=""}在 Prometheus 的模型里,「标签不存在」和「标签值为空字符串」是等价的。这个特性很有用——比如筛出那些还没打上 team 标签的服务:
count by (job) (up{team=""})=~ 需要对所有候选序列逐个跑正则,比 = 慢得多。在大规模环境里:
# 🐢 慢:要扫描所有指标的所有序列
{__name__=~"node_.*"}
# 🚀 快:先用精确匹配缩小范围,再用正则
node_cpu_seconds_total{mode=~"user|system"}原则:至少有一个精确匹配器(通常是指标名)来缩小候选集,再用正则做细筛。
另外,status=~"200|404|500" 这类纯枚举的正则,Prometheus 会自动优化成集合查找,性能接近精确匹配,不用担心。
4.4 指标名也是标签
第 4 章提过:指标名存储为特殊标签 __name__。所以这两个查询完全等价:
http_requests_total{status="200"}
{__name__="http_requests_total", status="200"}这带来一个实用能力——对指标名做正则匹配:
# 所有 node_exporter 的 CPU 相关指标
{__name__=~"node_cpu.*"}
# 所有以 _total 结尾的指标(找出所有 Counter)
{__name__=~".*_total"}{} # ❌ 报错:至少需要一个匹配器
{job!=""} # ✅ 合法
{__name__=~".*"} # ⚠️ 语法合法,但会尝试返回全部序列 —— 别在生产环境执行Prometheus 要求选择器必须至少有一个能选中非空值的匹配器,防止你不小心把整个数据库拉出来。
即便如此,{__name__=~".+"} 这样的查询依然是合法的,在有几百万条序列的实例上执行会直接把 Prometheus 打挂。探索数据请用 /api/v1/label/__name__/values 接口或 Web UI 的指标下拉框,不要用通配查询。
5. offset:查询过去的数据
5.1 基本用法
offset 修饰符把查询的「时间基准」往前挪:
# 当前的请求总数
http_requests_total
# 1 小时前的请求总数
http_requests_total offset 1h
# 1 天前的 QPS
rate(http_requests_total[5m] offset 1d)注意区间向量的写法:offset 放在 [5m] 后面:
rate(http_requests_total[5m] offset 1d) # ✅
rate(http_requests_total offset 1d [5m]) # ❌ 语法错误5.2 offset 的经典用途:同比 / 环比
offset 最大的价值是做对比。因为它返回的还是即时向量,可以直接和当前值做运算:
# 环比:现在的 QPS 相对 1 小时前的变化率
(
sum(rate(http_requests_total[5m]))
-
sum(rate(http_requests_total[5m] offset 1h))
)
/
sum(rate(http_requests_total[5m] offset 1h))# 同比:现在 vs 上周同一时刻(避开工作日/周末的差异)
sum(rate(http_requests_total[5m]))
/
sum(rate(http_requests_total[5m] offset 7d))同比对比在告警里特别有用:
- alert: TrafficDropAnomaly
# 当前流量不到上周同期的 50% → 可能是上游故障或 DNS 问题
expr: |
sum(rate(http_requests_total[10m]))
/
sum(rate(http_requests_total[10m] offset 7d))
< 0.5
for: 15m
labels:
severity: warning
annotations:
summary: "流量相比上周同期下跌超过 50%"为什么用「上周同期」而不是「1 小时前」? 因为业务流量有明显的日周期和周周期。凌晨 3 点的流量本来就只有下午的 10%,用「1 小时前」做基线会在每天早高峰和晚低谷时疯狂误报。用 offset 7d 比较的是「上周同一个星期几的同一时刻」,基线最稳。
5.3 offset 的限制
限制一:offset 必须是正数常量。
http_requests_total offset 1h # ✅
http_requests_total offset -1h # ❌ 老版本报错(Prometheus 2.26+ 开启 --enable-feature=promql-negative-offset 后支持负 offset,用于查询「未来」的数据——只在有远程写回填数据时才有意义。)
限制二:offset 只能修饰选择器,不能修饰表达式。
# ❌ 错误:offset 不能加在函数结果上
rate(http_requests_total[5m]) offset 1h
# ✅ 正确:offset 加在选择器上
rate(http_requests_total[5m] offset 1h)
# ❌ 错误
sum(http_requests_total) offset 1h
# ✅ 正确
sum(http_requests_total offset 1h)限制三:offset 受数据保留期限制。
offset 30d 在一个 --storage.tsdb.retention.time=15d 的实例上永远返回空。做长周期同比要先确认保留期够长,或者接了 Thanos / VictoriaMetrics 这类长期存储。
5.4 @ 修饰符:锚定到绝对时间
Prometheus 2.25+ 引入了 @,可以把求值时刻锚定到一个绝对的 Unix 时间戳:
# 查询 2024-05-01 00:00:00 UTC 那一刻的值
http_requests_total @ 1714521600
# 配合 offset 使用(先 @ 后 offset)
http_requests_total @ 1714521600 offset 5m两个特别有用的内置写法:
# 查询范围的起点 / 终点
http_requests_total @ start()
http_requests_total @ end()一个非常实用的场景——在 Grafana 面板上算「相比时间范围起点增长了多少」:
sum(http_requests_total) - sum(http_requests_total @ start())| 基准 | 典型用途 | |
|---|---|---|
offset 1h | 相对当前求值时刻往前推 | 同比环比,随图表时间轴滑动 |
@ 1714521600 | 绝对时间点,固定不动 | 定位某次事故时刻,做基线对照 |
在 Grafana 画图时,offset 的效果是「整条曲线整体左移」,@ 的效果是「一条水平直线」(因为每个点取的都是同一时刻的值)。
6. 在 Web UI 里试跑
理论讲完了,最快的学习方式是打开 Prometheus 自带的 Web UI 亲手试。
6.1 打开 Graph 页面
# 假设 Prometheus 跑在本机
open http://localhost:9090/graph界面上有几个关键元素:
┌──────────────────────────────────────────────────┐
│ [ 查询输入框 ] [Execute]│
│ ▸ Metrics explorer(指标下拉,可搜索) │
├──────────────────────────────────────────────────┤
│ [ Table ] [ Graph ] ← 两个标签页 │
│ 时间范围: [1h ▾] 分辨率: [auto] Stacked □ │
└──────────────────────────────────────────────────┘- Table 标签页:执行的是即时查询(instant query),显示某一时刻的值。区间向量只能在这里看。
- Graph 标签页:执行的是区间查询(range query),在时间轴上每隔一个步长求值一次,画成曲线。
6.2 循序渐进的试跑路径
# ① 最简单:看所有目标的健康状态
up
# ② 加匹配器:只看某个 job
up{job="node"}
# ③ 找异常:哪些目标挂了
up == 0
# ④ 数一数:一共有多少个目标
count(up)
# ⑤ 换个 Counter,先看原始值(Table 页)
prometheus_http_requests_total
# ⑥ 切到 Graph 页,你会看到一堆上升的斜线 —— 这就是「Counter 不套 rate」的样子
# ⑦ 套上 rate,再看 Graph —— 这才是有意义的图
rate(prometheus_http_requests_total[5m])
# ⑧ 加个 offset 做对比
rate(prometheus_http_requests_total[5m] offset 1h)
# ⑨ 试试区间向量直接查(Table 页可以,Graph 页会报错)
prometheus_http_requests_total[2m]6.3 用 API 做同样的事
Web UI 背后就是 HTTP API,用 curl 也可以:
# 即时查询
curl -s 'http://localhost:9090/api/v1/query?query=up' | jq
# 带 URL 编码的复杂查询
curl -s --data-urlencode 'query=rate(prometheus_http_requests_total[5m])' \
http://localhost:9090/api/v1/query | jq '.data.result[0]'
# 区间查询(画图用的那种)
curl -s 'http://localhost:9090/api/v1/query_range' \
--data-urlencode 'query=up' \
--data-urlencode 'start=2024-05-01T00:00:00Z' \
--data-urlencode 'end=2024-05-01T01:00:00Z' \
--data-urlencode 'step=60s' | jq
# 列出所有指标名(探索数据的正确姿势)
curl -s http://localhost:9090/api/v1/label/__name__/values | jq -r '.data[]' | head -30
# 查看某个指标有哪些标签值
curl -s 'http://localhost:9090/api/v1/label/job/values' | jq技巧一:Ctrl + Enter 执行查询,不用去点 Execute 按钮。
技巧二:Metrics explorer(输入框旁的图标) 可以模糊搜索所有指标名,比自己猜名字快得多。输入 cpu 就能看到所有含 cpu 的指标。
技巧三:出错时看 /targets 和 /tsdb-status。
/targets:所有抓取目标的状态和最后一次抓取的错误信息。查不到数据先看这里。/tsdb-status:序列数 Top10 的指标和标签,排查基数问题的第一站。
判断下列每个表达式的结果类型(即时向量 / 区间向量 / 标量 / 报错),如果报错说明原因:
1. node_load1
2. node_load1[10m]
3. rate(node_load1[10m])
4. rate(node_load1)
5. sum(node_load1[10m])
6. avg_over_time(node_load1[10m])
7. up{job="api"} offset 30m
8. rate(http_requests_total offset 1h [5m])
9. http_requests_total[5m] offset 1h
10. 42根据描述写出对应的 PromQL 选择器:
- 选出
job为api或web的所有up序列 - 选出所有 4xx 和 5xx 的 HTTP 请求(指标
http_requests_total,状态码在status标签) - 选出
apijob 中,实例 IP 以10.0.2.开头的请求指标 - 选出所有没有打
team标签的目标 - 选出所有文件系统指标,但排除
tmpfs和overlay类型(标签fstype) - 选出指标名以
go_gc开头的所有指标 - 选出
apijob 中,路径不是健康检查(/health、/healthz、/ready)的请求
你负责一个电商网站的监控。需求如下:
- 写一个查询,显示当前 QPS 和 1 天前同一时刻的 QPS 的比值。
- 解释为什么用
offset 1d比offset 1h更适合做流量异常检测。 - 写一条告警规则:当前订单创建速率(
orders_created_total)不到上周同期的 60% 时告警。 - 上面的告警在什么情况下会误报?如何改进?
你在 Grafana 里写了这个查询,但面板一片空白:
rate(myapp_http_requests_total{job="myapp", env="prod", status=~"5"}[30s])已知:应用确实在运行,Prometheus 抓取间隔是 15s,应用确实有 5xx 错误。
请列出排查步骤,找出所有可能的问题并给出修正后的查询。
小结
- PromQL 有四种类型:即时向量(每序列一个值)、区间向量(每序列一段样本)、标量、字符串。类型不匹配是最常见的报错来源。
- 即时向量按「向前回溯最近一个样本」取值,回溯上限默认 5 分钟(lookback delta)——这解释了目标下线后图表为何还会拖 5 分钟。
- 区间向量不能直接画图,它的唯一用途是喂给
rate/increase/*_over_time这类函数。区间长度建议 ≥ 抓取间隔 × 4,Grafana 里优先用$__rate_interval。 - 四种匹配器
=/!=/=~/!~,多个匹配器之间是 AND 关系。正则是完全匹配的(自动加^$),status=~"5"匹配不到500。 - 标签值为空
label=""同时匹配「标签不存在」,可用于找出缺失元数据的目标。指标名等价于__name__标签,可对它做正则。 offset把求值时刻相对往前推,是同比环比的基础;@锚定到绝对时间点。两者都只能修饰选择器,且要写在[区间]之后。- 所有「比值型」告警都应配一个绝对量下限条件,防止低流量时段的除法误报。
- 遇到查不到数据,去 Web UI 把查询拆到最简,再逐个条件加回去。