news 2026/10/16 11:00:45

PromQL实战:高频查询表达式与监控告警排障经验

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PromQL实战:高频查询表达式与监控告警排障经验

干过几年监控告警的人大概都有这种经历:线上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时间,同样需要rate
  • container_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]) * 8

3.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) > 90

4.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页面里调试,一边看数据曲线一边改表达式。曲线一出来,你立刻能看出“这步算出来的值是不是符合直觉”。等表达式在图上看着合理了,再复制到告警规则里验证阈值,这样能省掉很多面向监控面板猜谜的时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 10:59:02

CLion下STM32+RS485实现Modbus从站:从协议到调试全流程

简介&#xff1a;STM32-RS485-Modbus 是一份基于 STM32 HAL 库的嵌入式通信工程源码&#xff0c;围绕 RS485 总线接口与 Modbus RTU 协议实现展开&#xff0c;并演示了中菱轮毂电机 ZLAC8015D 的控制方法。项目在 CLion 环境中以 CMake 构建&#xff0c;代码结构清晰&#xff0…

作者头像 李华
网站建设 2026/10/16 10:58:16

Java @Override报错排查:方法未重写超类方法的常见原因与修复策略

如果你用过Override注解&#xff0c;大概率见过这句话&#xff1a;「方法未从其超类重写方法」。它的英文原文是method does not override method from its superclass&#xff0c;是 Java 编译器在编译阶段报出来的一个典型错误。说实话&#xff0c;我第一次遇到这个报错时完全…

作者头像 李华
网站建设 2026/10/16 10:58:05

支付宝手机网站支付接入实战:异步通知与订单闭环处理

1. 支付接入前的基础认知与整体设计思路1.1 为什么支付宝手机网站支付值得单独梳理一套接入逻辑做Java后端开发的朋友&#xff0c;只要碰过电商、知识付费、会员充值这类带交易闭环的项目&#xff0c;迟早要面对支付接入。支付宝的手机网站支付&#xff08;alipay.trade.wap.pa…

作者头像 李华
网站建设 2026/10/16 10:57:32

sklearn线性回归大家族:从LinearRegression到鲁棒模型的实战选型指南

1. 这不是“线性回归入门”&#xff0c;而是你真正用得上的线性回归工具箱“sklearn中的线性回归大家族”——光看标题&#xff0c;很多人第一反应是&#xff1a;“不就是LinearRegression吗&#xff1f;调个fit、predict&#xff0c;写三行代码的事。”但我在某高校数据科学实…

作者头像 李华
网站建设 2026/10/16 10:56:50

纯前端模拟大模型对话交互:本地响应池与流式输出实战

先从实际工作中的一个场景说起。开发一个带大模型对话功能的页面时&#xff0c;最尴尬的阶段往往是"前端写完了&#xff0c;后端还没联调"&#xff0c;或者"产品想先看效果&#xff0c;但API密钥还没申请下来"。如果你也遇到过这种局面&#xff0c;那么&qu…

作者头像 李华
网站建设 2026/10/16 10:56:42

Flink键控流状态编程:Keyed State原理与实战

做实时计算&#xff0c;最绕不开的两个词&#xff0c;一个是时间&#xff0c;另一个就是状态。最近连续处理了几个实时指标需求&#xff0c;每次都是在键控流&#xff08;KeyedDataStream&#xff09;上挂状态来完成的&#xff0c;搞得不少刚上手 Flink 的同事跑来问我&#xf…

作者头像 李华