跳到主要内容

PromQL 查询语言

前面两篇把 Prometheus 架起来、也讲清了 数据模型与抓取原理,但日常用 Prometheus,八成时间其实花在写查询上:画图、配告警、排障看数,全是 PromQL。这篇笔记按「数据长什么样 → 怎么选序列 → 必会函数 → 实战菜谱」的顺序,把日常要用到的写法过一遍。示例都可以直接粘到表达式浏览器(Web 界面的 Graph 页,http://localhost:9090/graph)里跑。环境口径:Prometheus 2.5x / 3.x,node_exporter 1.x。

数据模型​

指标名 + 标签集 = 一条时间序列​

Prometheus 里存的所有数据都是时间序列。一条序列由两部分唯一确定:

  • 指标名(metric name):说明这是什么指标,比如 http_requests_total、node_cpu_seconds_total
  • 标签集(labels):一组 key=value 对,描述这条序列的维度,比如 instance="10.0.0.1:9100"、job="node"

同一个指标名、标签只要有一个不同,就是另一条序列。比如 http_requests_total{job="api", handler="/users"} 和 http_requests_total{job="api", handler="/orders"} 是两条独立的序列,各自计数、互不影响。

一个样本(sample)就是一个「时间戳 + 值」的二元组:毫秒精度的 Unix 时间戳,加一个 float64 的值。序列就是样本按时间排起来的流。查询的本质,就是从这些序列里选一批、算一算。

查询结果的类型​

在表达式浏览器里敲任何查询,结果只有四种类型:

类型例子说明
瞬时向量 instant vectorhttp_requests_total一组序列,每条只有「查询时刻」的一个样本
区间向量 range vectorhttp_requests_total[5m]一组序列,每条带最近 5 分钟的一串样本
标量 scalar1024、单位换算里的常数单个浮点数,没有标签
字符串 string基本用不到单个字符串

记住两条硬规则:

  • 告警规则和画图用的是瞬时向量;区间向量不能直接拿去告警,它只是函数的输入
  • rate()、increase() 这类「看一段时间变化」的函数只吃区间向量,所以括号里永远带着 [5m] 这样的区间
瞬时向量的「当前值」其实是回看的

查询时刻通常不会正好踩到样本。Prometheus 会从查询时刻向前回看最多 5 分钟(lookback delta),取每条序列的最后一个样本。所以 target 刚下线时它的值还会「残留」最多 5 分钟——这不是 bug,是设计如此。

选择器​

标签匹配的四种写法​

选中一批序列靠标签匹配器,一共四种:

匹配器含义例子
=等值up{job="prometheus"}
!=不等http_requests_total{status!="200"}
=~正则匹配http_requests_total{status=~"5.."}
!~正则不匹配node_filesystem_size_bytes{fstype!~"tmpfs|overlay"}

两个要点:

  • 正则是完全锚定的:status=~"5" 只匹配值恰好是 "5" 的序列,想匹配 5xx 要写 5..。正则引擎是 RE2,不支持反向引用
  • 标签值为空等于没有这个标签:env="" 选中的是「没有 env 标签」的序列,配合 != / =~ 排查标签缺失时很有用

只写指标名 up 是选这个指标的全部序列;理论上也可以只写匹配器不带指标名,但 {job=~".+"} 这种写法等于全表扫描,生产上别用。

区间选择​

选择器后面加 [时长],瞬时向量就变成区间向量:

# 最近 5 分钟的所有样本
http_requests_total[5m]

# 带标签过滤的区间向量
node_cpu_seconds_total{mode="idle", cpu="0"}[10m]

时长单位:s 秒、m 分、h 时、d 天、w 周、y 年,可以组合成 [1h30m]。区间向量基本只作为函数输入;直接在表达式浏览器跑也能返回,但那是一堆原始样本点,没什么可读性。

offset 偏移​

offset 把查询的参考时刻整体往回挪,用来做历史同期对比:

# 现在的 QPS
sum(rate(http_requests_total[5m]))

# 一小时前同时段的 QPS
sum(rate(http_requests_total[5m] offset 1h))

# 差值:比一小时前涨了多少
sum(rate(http_requests_total[5m]))
-
sum(rate(http_requests_total[5m] offset 1h))

排障时常用「现在 vs 昨天 / 上周同时段」确认问题是不是周期性的。另外还有个 @ 修饰符能把查询钉在某个绝对时间戳上,日常用得少,知道有这东西就行。

必会函数​

rate、increase、irate:Counter 三件套​

Counter 类型(xxx_total 结尾)的值只增不减,原始值本身没意义,必须算「涨得多快」。三个函数的区别:

函数算法适用场景
rate()区间内首尾差 ÷ 时长,算每秒平均增速画图、告警,绝大多数场景
increase()rate() 结果 × 区间秒数,算这段时间涨了多少总量「最近 1 小时处理了多少请求」
irate()只用区间内最后两个样本算瞬时增速排障看瞬时尖刺
# 每台机器每秒的 HTTP 请求数
rate(http_requests_total[5m])

# 最近 1 小时总共处理了多少请求
increase(http_requests_total[1h])

经验法则一句话:默认用 rate;要看总量用 increase;irate 只在排障看尖峰时用。告警别用 irate——它太灵敏,一个抖动就触发。

sum、avg、max、min 与 by / without 分组​

聚合运算符把多条序列合并成少数几条,by 和 without 控制按什么维度分:

  • by (标签...):只保留列出的标签,按它们分组
  • without (标签...):去掉列出的标签,按剩下的分组
# 全部实例的总 QPS(合并成一条线)
sum(rate(http_requests_total[5m]))

# 按实例分开看 QPS(每台一条线)
sum by (instance) (rate(http_requests_total[5m]))

# 按 job 看,忽略实例和接口维度
sum without (instance, handler) (rate(http_requests_total[5m]))

avg / max / min / count 用法完全一样。顺序要记死:rate 写在 sum 里面。直觉上「先加总再求速率」的 rate(sum(...)) 直接是语法错误——而且就算能写,先加总在 Counter 重启归零时也会算错。

topk / bottomk​

从一堆序列里挑出最大或最小的 k 条:

# CPU 使用率最高的 5 个实例
topk(5, 100 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

# 剩余磁盘最少的 3 个挂载点
bottomk(3, node_filesystem_avail_bytes)

k 是常量,序列不足 k 条时返回全部。画图时 topk 很常用——几百条序列直接糊在一张图上没法看,只画头部几条。

histogram_quantile:算 P95 / P99 延迟​

直方图指标(xxx_bucket 结尾、带 le 标签)的 quantile 计算是 PromQL 里最绕的一段,拆开看:

histogram_quantile(0.99,
sum by (le) (rate(http_request_duration_seconds_bucket{job="api"}[5m])))

从内到外逐段解释:

  1. http_request_duration_seconds_bucket{job="api"}:选出一组桶序列,每条带一个 le(less than or equal)标签,le="0.1" 表示「耗时不超过 0.1 秒的请求累计数」。桶是累计的:le="0.5" 的计数包含了所有更快桶里的请求
  2. rate(...[5m]):算每个桶每秒新增多少请求,把只增不减的累计数变成速率
  3. sum by (le) (...):把同一 le 值下不同实例、不同接口的桶合并,只留 le 这一个维度——这步必须有,少了它 quantile 没法算
  4. histogram_quantile(0.99, ...):在相邻桶之间做线性插值,估算出「99% 的请求比这个值快」
histogram_quantile 的精度上限是桶边界

估算值只能落在桶边界附近。如果最粗的桶是 le="10",实际 P99 再高也只报 10 秒。想要准的 P99,埋点时桶的划分要覆盖你关心的延迟区间。

配套的 _sum 和 _count 序列还能算平均延迟:

# 平均延迟 = 总耗时 / 请求总数
sum(rate(http_request_duration_seconds_sum[5m]))
/
sum(rate(http_request_duration_seconds_count[5m]))

absent:检测「没有数据」​

告警里最怕的不是值异常,而是序列整个没了——指标消失往往说明 exporter 挂了或者目标掉了,这本身就是故障。absent() 干的就是这个:

# 这个 job 一条序列都没有时返回 1,有数据时返回空
absent(up{job="api"})

# 检测某个具体指标是否消失
absent(http_requests_total{job="api"})

absent 的语义和「值为 0」完全不同:没数据时返回一条值为 1 的序列,有数据时返回空。判断「target 是不是全挂了」用 absent(up{job="xxx"}) 比拿 up 的值去比 0 可靠——后者在序列消失时什么都报不出来。

运算符​

算术与比较​

算术运算符 + - * / % ^ 没太多好说的,两边可以是标量或向量。常用的「标量换算向量」:

# 字节换算成 GiB
node_memory_MemAvailable_bytes / 1024^3

# 增速换算成百分比
rate(node_cpu_seconds_total{mode="idle"}[5m]) * 100

比较运算符 == != > < >= <= 的默认行为是过滤:只保留满足条件的序列,值原样保留:

# 只返回磁盘剩余不足 100 GiB 的挂载点,值还是字节数
node_filesystem_avail_bytes < 100 * 1024^3

加个 bool 修饰符,行为变成判定:每条序列都保留,满足条件值为 1、不满足为 0:

# 每条序列返回 0 或 1,适合统计有多少台不达标
node_filesystem_avail_bytes < bool 100 * 1024^3

# 数一下有几台磁盘告急
count(node_filesystem_avail_bytes < bool 100 * 1024^3)

告警用过滤语义(报出来的值是原始值,可读性好);统计「有几台超标」这类需求用 bool。

逻辑运算符​

三个集合运算,作用于向量之间:

运算符语义典型用途
and交集:两边都有才保留多条件同时成立才告警
or并集:任一边有就保留合并多条告警表达式
unless差集:左边有、右边没有才保留排除已知问题
# 磁盘空间不足 AND inode 也不足,才告警
(node_filesystem_avail_bytes / node_filesystem_size_bytes < 0.1)
and
(node_filesystem_files_free / node_filesystem_files < 0.1)

向量与向量运算时默认按标签完全匹配来对齐序列;两边标签集对不上时要用 on() / ignoring() 显式指定匹配键,属于进阶写法,用到再查文档。

实战菜谱​

每道菜一行查询加一句话场景,直接粘到表达式浏览器就能跑。

1. CPU 使用率(%)

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

1 减去 idle 占比就是 busy 占比;avg by (instance) 把每台机器的所有核合成一条线。

2. 内存使用率(%)

(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100

用 MemAvailable 而不是 MemFree:前者把可回收的 cache/buffer 算作可用,才是真实可用内存。

3. 磁盘 4 小时后会不会满

predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4 * 3600)

按最近 6 小时的线性趋势外推,预测 4 小时后的剩余字节数;结果小于 0 就是「撑不过 4 小时」,告警表达式常配 < 0。

4. 接口 QPS

sum by (handler) (rate(http_requests_total[5m]))

按接口分开看每秒请求数;想看总量就把 by 子句去掉。

5. 错误率(5xx 占比,%)

sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) * 100

分子分母用同一个区间长度,比值才有意义;流量太小时比例会乱跳,配告警要留神。

6. P99 延迟(秒)

histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))

中间的 sum by (le) 不能省——直接对原始桶序列算 quantile 会按实例、接口各自分开插值,结果没有意义。

7. 最近 5 分钟有变化的序列

changes(http_requests_total[5m]) > 0

changes() 返回区间内值变了几次,大于 0 说明这条序列还活着;排查「哪些指标停了」时反着用,找等于 0 的。

排错与优化​

查询慢、浏览器卡死、Prometheus OOM,九成是基数(cardinality)问题——标签组合太多。定位手段:

  • Web 界面 Status → TSDB Stats 页面能看 Head 里的序列总数,以及按指标名、按标签的 Top 10 统计,一眼看出哪个指标爆炸
  • 表达式浏览器的 Table 标签页会显示本次查询加载了多少条序列,上了万级就该警惕
  • 常见凶手:把 user_id、session_id、带参数的 URL 路径做成标签值,一个指标轻松上百万条序列

写法上的几个原则:

  1. 先过滤再聚合:sum(rate(http_requests_total{job="api"}[5m])) 比不带过滤条件的版本少算一堆无关序列
  2. 别选全量:不带指标名、只写匹配器的查询等于全表扫描,生产面板里不要出现
  3. rate 区间别贪小:[1m] 配 30 秒抓取间隔,区间内可能只有两三个样本,算出来全是毛刺还白加重计算
  4. 重查询下沉成 Recording Rules:面板和告警反复用到的复杂表达式,预计算成一个新指标,查询时直接取
# recording rule 示例:把每台实例的 CPU 使用率预计算出来
groups:
- name: node_rules
interval: 30s
rules:
- record: instance:node_cpu_utilization:ratio
expr: 1 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))

之后查询直接写 instance:node_cpu_utilization:ratio,不用每次都扫原始序列,面板加载和告警评估都快得多。

常见坑​

按踩坑频率排:

  1. rate 区间太短全是毛刺。rate 只用区间内首尾样本做差分,区间太短、样本太少,一点抖动就被放大成尖峰。经验值:区间至少 4 倍抓取间隔——默认 15 秒抓取就 [1m] 起步,画图告警常用 [5m]。
  2. increase 算出小数不是 bug。increase(http_requests_total[5m]) 返回 12.7 很正常:它是 rate 乘时长算出来的外推值,不是逐样本加出来的整数。想要更贴近真实计数就拉长区间,或者接受这是估计值。
  3. Counter 重启归零不用特殊处理。exporter 重启后计数器从 0 重新来,rate/increase 内置了 counter reset 检测:发现值变小就当「归零后重新计数」处理,不会算出负数尖峰。进程重启了,查询照常写就行。
  4. 值的时间戳和想象的不一样。瞬时向量是「查询时刻向前回看最多 5 分钟取最后一个样本」,所以 target 下线后值会残留最多 5 分钟,图上某个点的实际采样时间也可能比显示时刻早。对时间线排障时心里要有这根弦。
  5. 除法或运算结果是空的,不是报错。a / b 里 b 在该时刻没有序列,结果就是空,图上直接断线。常见原因是两边标签集对不上、或者一边的过滤条件把序列筛没了。空结果在 PromQL 里不是错误——悄悄没数据最坑人,排查时把表达式拆成两半分别跑一遍。
写复杂查询先拆着验

一个多层嵌套的表达式算不出结果时,从最内层开始逐层粘到浏览器里跑,确认每一层选出来的序列和值都对,再往外包。比盯着整条表达式干看快得多。

小结​

  • 数据模型一句话:指标名 + 标签集确定一条序列,样本是「时间戳 + 值」;查询结果记两种向量——瞬时向量用于告警画图,区间向量只做函数输入
  • 选择器四种匹配 =、!=、=~、!~,正则完全锚定;[5m] 转区间向量,offset 看历史同期
  • Counter 必配 rate:画图告警用 rate(),看总量用 increase(),irate() 只在排障看尖峰时用
  • 聚合顺序记死:sum(rate(...)),rate 永远在内层;直方图算延迟必须有 sum by (le)
  • absent() 管「指标消失」;比较运算符默认过滤序列,加 bool 返回 0/1
  • 慢查询先怀疑基数:TSDB Stats 看序列数,先过滤再聚合,重查询下沉 recording rules
  • rate 区间至少 4 倍抓取间隔,increase 出小数正常,Counter 归零被自动处理

下一篇讲告警:Alertmanager 的路由、分组与静默,以及怎么把这篇写的表达式变成不吵人的告警规则。