干过几年监控告警的人大概都有这种经历:线上CPU飙到100%,Prometheus里数据明明全都有,可就是写不出一条能准确表达“CPU到底是谁在用”的promeQL查询。promeQL是Prometheus监控体系里的查询语言,它的作用就是在一堆带时间戳和标签的时序数据里,把你要的指标捞出来、参与计算、再做聚合,最终落到告警规则或者Grafana面板上。这篇文章我把自己在日常排障、配告警、写图表中用得最多的promeQL查询表达式整理了一遍,适合正在调试Prometheus告警规则的平台同学,也适合刚接触监控系统、想快速上手查询套路的新人。
下面不会从头讲一遍语法手册,而是按我实际工作时的思路来:先理解promeQL的数据模型,再给出直接能用的高频查询,最后把踩过的坑原原本本摊开来说。
1. 理解promeQL的核心:不只是“查数据”,而是“算数据”
1.1 时序数据长什么样
promeQL跟SQL最大的区别,是它处理的是“随时间不断追加的数值序列”,而不是一张二维表。每个指标本质上是一个带标签集合的时间序列。
一个完整的时间序列由三部分组成:
- 指标名:例如
node_cpu_seconds_total,描述“测的是什么” - 标签集合:例如
instance="192.168.1.10:9100"、mode="idle"、cpu="0",描述“这一份数据来自哪里、属于哪个维度” - 采样值:每一个时间戳上对应的数值,以及采样时间
也就是说,node_cpu_seconds_total{cpu="0",mode="idle"}其实是一条独立的时间序列,它和node_cpu_seconds_total{cpu="1",mode="idle"}是两条完全不同的序列。
很多新手写promeQL时犯的第一个错误,就是拿SQL的思维去套:以为“select 某个指标 from 某个表”就是全部。但实际写promeQL时,你大多数时间是在“从同一批指标里选出若干序列、做数学运算、再按维度合并”,这个过程非常考验对标签集合的理解。
打个比方:SQL里的表是已经排好的一张Excel表,你只需要挑选列和行;而promeQL更像是给你一大堆碎片卡片,每张卡片上写着“这个机器、这个CPU核、在idle模式下累计消耗了多少秒”。你要做的是先按标签筛出一堆卡片,再在卡片之间做加减乘除,最后按某个维度把它们摞起来看结果。
1.2 指标类型决定了你能怎么算
Prometheus客户端库里有四种指标类型,promeQL的很多函数和运算逻辑都跟类型强相关。
- Counter(计数器):只增不减。比如请求总数、CPU累计时间。因为它是累计值,直接取原始值没有意义,必须用
rate、increase这类函数算“增速”。 - Gauge(仪表盘):可增可减,反映当前状态。比如当前内存使用量、当前温度,直接查询就有意义。
- Histogram(直方图):一组带
_bucket后缀的计数器,用于计算分位数。 - Summary(摘要):客户端直接算好的分位数,查询方式跟Gauge接近。
有一个经常被忽略的坑:Counter类型如果服务重启,会从0重新开始计数,导致曲线出现阶梯式下跌。如果直接对它做increase,结果可能在某些区间出现“负增长”,所以务必要结合时间窗口和rate来处理。
1.3 瞬时向量和区间向量:必须先搞懂
promeQL查询会得到两种结果:瞬时向量和区间向量。
- 瞬时向量:表示“当前这一刻”每个序列的采样值。比如直接查
node_memory_MemTotal_bytes,看到的是最新采样的值。 - 区间向量:表示“过去一段时间内”每个序列的多个采样点。比如
node_memory_MemTotal_bytes[5m],返回的是最近5分钟每个抓取周期产生的采样值集合。
区间向量不能直接拿来画曲线,因为它的每个元素不是一个数,而是一串数。通常需要配合rate、increase、avg_over_time等函数把它转换成瞬时向量。这也是新手最常见的报错场景:“query didn't return series”或者“expression: vector cannot contain multiple values per label”之类,大多是因为在需要瞬时向量的地方用了区间向量,或者反过来。
理解这两者的区别,后面所有查询表达式才有立足点。
2. 基础查询与过滤:先把自己要的序列捞出来
2.1 指标选择器和标签匹配器
最简单的promeQL就是直接写指标名:
node_memory_MemTotal_bytes这样会返回该指标下所有标签组合的序列。大多数时候你不需要全量数据,而是要按标签过滤:
node_memory_MemTotal_bytes{instance="10.0.0.5:9100"}promeQL的标签匹配器支持四种操作符:
| 操作符 | 含义 | 示例 |
|---|---|---|
= | 精确匹配 | instance="10.0.0.5:9100" |
!= | 不匹配 | instance!="10.0.0.5:9100" |
=~ | 正则匹配 | instance=~"10.0.0.*:9100" |
!~ | 正则不匹配 | instance!~"10.0.0.*" |
正则匹配用的语法是RE2,不支持贪婪匹配之类的PCRE高级特性,也不支持回溯。所以写复杂正则时要格外小心,比如.*cpu.*这种写法看着没问题,但量一大就会拖慢查询速度。
实际工作中我常用=~"10.0.0.*:9100"来一次选中一个网段的机器,或者用job=~".*node.*"把同一类Exporter的序列都捞出来。
2.2 空标签和标签存在性
有一个很微妙的点:{label=""}表示“该标签存在但值为空”,而不能表示“该标签不存在”。之所以要区分,是因为Prometheus在存储层把所有标签都建成了索引,标签不存在和标签值为空,在查询语义上是两回事。
举个例子,如果某个指标本身没有slot标签,你写{slot=""}是查不到任何序列的。想查“没有某个标签的序列”,常规做法是反向正则:
some_metric{slot!~".+"}意思是选择slot为空或标签不存在的序列。这种写法在清理一些不规范的Exporter数据时很常用。
2.3 基础运算:算术、比较、逻辑
选择好序列之后,真正的工作才刚刚开始。promeQL支持将多个序列直接做运算,运算是按照“相同的标签集合”去匹配的。
CPU使用率之所以写成1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])),本质上就是“把所有CPU核的时间加起来,减去idle空闲时间,剩下的就是被占用的”。写成表达式就是:
1 - avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))算术运算+ - * /可以作用于两个向量,也可以作用于向量和标量。比较运算> >= < <= == !=的作用更像过滤器:比如node_load1 > 4会把“负载大于4”的序列保留下来,其他序列则被丢弃。
逻辑运算and、or、unless用于向量的集合操作,在复杂查询里很常见。
3. 高频场景实操:CPU、内存、磁盘、网络、业务指标
3.1 CPU使用率
先看最经典的CPU使用率查询。注意,凡是涉及cpu的时间累计序列,底层都是Counter,必须先rate再使用:
100 - avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100解释一下为什么用“1减idle”:node_cpu_seconds_total包含user、system、iowait、idle、steal等多个mode,如果直接把所有mode加一起再求占比,你会把idle也算成“使用”,结果永远接近100%。所以标准做法是算出idle占比,再用100减去它。
如果只想看某个CPU核:
100 - avg by(cpu, instance) (rate(node_cpu_seconds_total{mode="idle", cpu="0"}[5m])) * 100容器场景则看这两个指标:
container_cpu_usage_seconds_total:容器累计CPU时间,同样需要ratecontainer_spec_cpu_quota/container_spec_cpu_period:容器CPU限额配置
容器CPU使用率的常见写法:
rate(container_cpu_usage_seconds_total{container!=""}[5m]) / container_spec_cpu_quota{container!=""} * 100这里过滤掉container=""很关键,因为容器运行时经常会暴露一组空的容器标签,不滤掉会把所有Pod的汇总数据都算进去,结果直接翻倍。
3.2 内存使用
内存指标类型是Gauge,所以不需要rate。宿主机内存使用率:
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100这里我特意用了MemAvailable而不是MemFree,因为MemFree只是完全空闲的物理内存,系统里还有很多可回收的缓存和page cache。MemAvailable是内核经过估算后真正“可以被分配出去”的内存,用它算使用率才不会出现“明明内存很空,却显示用了80%”的怪现象。
容器内存用的是:
container_memory_working_set_bytes{container!=""}working_set_bytes表示容器当前实际占用的物理内存,比container_memory_usage_bytes更贴近“用了多少”的真实含义。后者的值包含了page cache,当文件读取频繁时会明显偏高,容易误报。
3.3 磁盘空间和磁盘IO
磁盘空间查询注意两点:过滤掉虚拟文件系统,按挂载点聚合。
100 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|squashfs"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay|squashfs"} * 100不把tmpfs和overlay过滤掉的话,/proc、/sys这类虚拟文件系统会混进来,出现大量“使用率100%/0%”的假序列,告警基本没法看。
磁盘IO则用:
rate(node_disk_read_bytes_total{device=~"sd.*|vd.*|nvme.*"}[5m])这里用正则锁定物理磁盘设备名,避免把分区统计造成的数据重复加进去。
3.4 网络流量
网络流量本身就是Counter,需要rate转成速率:
rate(node_network_receive_bytes_total{device!="lo"}[5m]) * 8为什么乘以8?因为后面如果要出Mbps单位,字节要先转成比特。Grafana面板里可以直接设data rate,但如果你在告警表达式里需要比较阈值,乘8这步必须自己算清楚。
出口流量同理:
rate(node_network_transmit_bytes_total{device!="lo"}[5m]) * 83.5 业务指标:请求量、错误率、延迟
业务指标的命名通常没有统一标准,这里用一个常见的HTTP请求计数器举例:
- 请求总量:
sum(rate(http_requests_total[5m])) - 5xx错误量:
sum(rate(http_requests_total{status=~"5.."}[5m])) - 错误率:
sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))
注意:分子分母都用了sum,这样除法会先各自聚合成一个值再运算。如果漏掉分子或分母的sum,promeQL会尝试按标签进行向量匹配,结果要么报错,要么算出N个不同的错误率,告警根本没法收敛。
4. 聚合操作与进阶函数:从数据里提炼结论
4.1 rate、irate、increase:到底用哪个
这是promeQL新手最容易纠结的一组函数。
rate():计算区间窗口内每秒钟的平均增长率,它会基于窗口的起止值做平滑计算。irate():取区间内最后两个采样点计算瞬时增长率,能更敏感地反映突发变化。increase():计算区间内总共增加了多少,可以理解为rate() * 窗口秒数的近似值。
我的建议是:
- 画趋势图、看长时间波动,用
rate - 看瞬时突刺、排障时快速定位异常波动,用
irate - 看“这半小时总共多了多少请求”,用
increase
irate有一个反直觉的坑:窗口太短且两次采集间隔不稳时,它会被两个点的微小抖动放大,画出来的图毛刺非常多。比如irate(node_cpu_seconds_total[1m])在这种抓取频率下就很不稳定,一般建议窗口至少是抓取间隔的4倍以上。
4.2 by和without:聚合的关键字
sum、avg、max、min、count这些聚合函数,默认会把所有标签维度全并成一个值。想保留某些维度,就必须用by或without。
by(instance):结果只保留instance标签without(instance):结果保留除了instance之外的所有标签
两者作用相反,但效果略有差异。如果标签集合比较杂,without更容易漏关注面。举一个实际场景:查每个实例的CPU使用率:
100 - avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100因为只想看“每台机器”,所以用by(instance)把cpu、mode这两个维度去掉。
如果改成without(cpu, mode),效果一样,但可读性差一些。我的经验是:优先用by,把要保留的维度显式列出来,逻辑最清晰,也不容易把job、device这类标省略掉。
4.3 topk:找出TOP N
定位“哪台机器在拖累整个集群”时,topk很有用:
topk(5, 100 - avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)但要注意,topk不是聚合函数,它和sum、avg机制完全不同。topk(k, vector)的结果是“保留值最大的k个序列”,不会把多个序列合并成一个。因此它只能用于展示和排序,不能用于后续运算,更不能直接写进告警规则做“当CPU最高那台机器超过阈值”的判断。
如果告警要“取最大值”,应该用max():
max(100 - avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 904.4 histogram_quantile:分位数延迟计算
HTTP延迟、数据库查询耗时分位数,通常用Histogram指标存。常见命名格式是_bucket,例如http_request_duration_seconds_bucket{le="0.1"}。
P99延迟的查询:
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))为什么这里要sum by(le)?因为Histogram分布在多个实例上,每个实例都有一组le桶,必须先按桶把多个实例的计数加总,再交给histogram_quantile计算整体分位数。如果漏掉sum by(le),函数会对每个实例分别算一个P99,结果可能是一堆曲线,无法代表整体。
分位数本身是估算值,不是精确值,所以不要指望histogram_quantile(0.99, ...)和日志里数出来的P99完全一致。当bucket划分不够细时,误差会很大。
4.5 Recording rules:把常用表达式固化
生产环境不太建议每个面板都直接跑这种多级聚合的promeQL,一是查询效率低,二是表达式写错一次,所有面板一起遭殃。
我的做法是维护一套Recording rules,把高频复杂查询预计算成新的指标。比如:
groups: - name: node.rules rules: - record: node:cpu_usage:percent expr: 100 - avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100 - record: node:memory_usage:percent expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100这样Grafana面板直接查node:cpu_usage:percent,告警表达式也可以大幅简化。这个思路类似于数据仓库里的物化视图,等于用Prometheus的定期计算换取了查询时的性能。
5. 常见问题与排查技巧实录
5.1 高频排查故障速查表
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 查询返回“No data” | 标签拼写错误、指标名不存在、区间窗口内无数据 | 先用{__name__=~".*关键字.*"}搜索指标名,再逐层缩小标签范围 |
| 结果值巨大/绝对值异常 | 忘了rate(),把Counter累计值当成Gauge用 | 检查指标类型,Counter必须先算增速 |
| CPU使用率总是100% | mode标签没过滤idle,把空闲也算进使用了 | 改用1 - idle占比的写法 |
| 容器内存远超实际 | 用了container_memory_usage_bytes包含page cache | 换成container_memory_working_set_bytes |
| 错误率曲线有很多毛刺 | 分子分母没有各自sum,导致按标签逐一相除 | 分子分母都先聚合再除法 |
| 查询结果“不连续” | 服务重启导致Counter重置,或抓取点丢失 | 使用rate且窗口大于抓取间隔,必要时配合increase观察 |
| 查询非常慢/超时 | 标签匹配写得太宽,或做了高基数聚合 | 缩小时间范围,用更精确的标签匹配,避免=~".*" |
5.2 排障时的三个实操技巧
第一个技巧:先缩小时间范围。把Grafana面板拉到最近15分钟,比在“过去7天”上调试表达式快得多,因为时间范围越大,底层要扫描的数据越多,查询越慢,报错也越难定位。
第二个技巧:用{__name__=~".*name.*"}搜索指标名。很多时候你只是记不清完整拼写,凭记忆写node_memmory_MemTotal,自然是No data。先用正则搜一下实际存在的指标名全称,能避免一半的低级错误。
第三个技巧:写复杂表达式前,先拆成两步验证。比如写错误率,先分别执行分子和分母的两条子查询,确认两者都有数据且维度正确,再合并成一条除法表达式。这样一旦结果不对,你能准确知道是哪一步出了问题。
5.3 关于step参数的一个隐蔽坑
在Grafana里查询promeQL时,step参数决定了下采样粒度。如果你手动把一个5分钟窗口的rate表达式,配上很短的step比如15秒,曲线看起来会像锯齿;反之step设得太大,又会丢掉细节。
promeQL本身对step的处理是“查询时点采样”。比如step=15s,它就会在时间轴上每隔15秒取一个点,然后在每个点上重新计算rate表达式。这意味着同一个表达式,不同step下结果可能差异很大。
所以在做告警阈值调试时,别只看数值大小,还要确认Grafana面板右下角的step设置到底是多少。调完step发现表达式结果对不上,这是很正常的情况,不用怀疑表达式写错。
5.4 告警规则里的额外注意点
写Alerts时,一个常见坑是表达式里用了短窗口,但告警规则本身设置了很长的for。比如表达式用[2m]窗口判断,却要求持续30分钟才触发,本质上是用2分钟的平均值去代表30分钟的持续状态,中间可能被瞬时尖刺骗过,也可能对真实异常不敏感。
比较合理的做法是告警表达式里的窗口和for保持一个量级,或者直接让告警表达式足够“钝”,别用太短的irate做阈值判断。我的习惯是:告警用rate()[5m],配合for: 5m,既避免抖动,又能较快响应。
6. 我的实际体会
写了几年promeQL,最深的感受是:绝大多数查询问题都不是语法不会,而是对“时序数据模型”理解不够。标签不是数据库里的索引概念,而是数据的一部分;Counter不是当前值,而是一个只有起点没有终点的累计值。把这些概念刻进脑子里,绝大多数表达式都能顺势写出来。
另一个很实用的经验是把常用查询收集成自己的“查询清单”。每次排障时,先把清单里写好的表达式拉一遍,先看机器,再看进程,最后看业务指标,基本能快速圈定问题范围。这套方法也建议你在自己团队里建立起来,别让查询表达式只存在各人的浏览器和便签里。
最后分享一个小技巧:如果你刚开始学promeQL,建议直接在Grafana的Explore页面里调试,一边看数据曲线一边改表达式。曲线一出来,你立刻能看出“这步算出来的值是不是符合直觉”。等表达式在图上看着合理了,再复制到告警规则里验证阈值,这样能省掉很多面向监控面板猜谜的时间。