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

📅 发布时间:2026/10/16 11:00:50
PromQL实战:高频查询表达式与监控告警排障经验
干过几年监控告警的人大概都有这种经历线上CPU飙到100%Prometheus里数据明明全都有可就是写不出一条能准确表达“CPU到底是谁在用”的promeQL查询。promeQL是Prometheus监控体系里的查询语言它的作用就是在一堆带时间戳和标签的时序数据里把你要的指标捞出来、参与计算、再做聚合最终落到告警规则或者Grafana面板上。这篇文章我把自己在日常排障、配告警、写图表中用得最多的promeQL查询表达式整理了一遍适合正在调试Prometheus告警规则的平台同学也适合刚接触监控系统、想快速上手查询套路的新人。下面不会从头讲一遍语法手册而是按我实际工作时的思路来先理解promeQL的数据模型再给出直接能用的高频查询最后把踩过的坑原原本本摊开来说。1. 理解promeQL的核心不只是“查数据”而是“算数据”1.1 时序数据长什么样promeQL跟SQL最大的区别是它处理的是“随时间不断追加的数值序列”而不是一张二维表。每个指标本质上是一个带标签集合的时间序列。一个完整的时间序列由三部分组成指标名例如node_cpu_seconds_total描述“测的是什么”标签集合例如instance192.168.1.10:9100、modeidle、cpu0描述“这一份数据来自哪里、属于哪个维度”采样值每一个时间戳上对应的数值以及采样时间也就是说node_cpu_seconds_total{cpu0,modeidle}其实是一条独立的时间序列它和node_cpu_seconds_total{cpu1,modeidle}是两条完全不同的序列。很多新手写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 didnt return series”或者“expression: vector cannot contain multiple values per label”之类大多是因为在需要瞬时向量的地方用了区间向量或者反过来。理解这两者的区别后面所有查询表达式才有立足点。2. 基础查询与过滤先把自己要的序列捞出来2.1 指标选择器和标签匹配器最简单的promeQL就是直接写指标名node_memory_MemTotal_bytes这样会返回该指标下所有标签组合的序列。大多数时候你不需要全量数据而是要按标签过滤node_memory_MemTotal_bytes{instance10.0.0.5:9100}promeQL的标签匹配器支持四种操作符操作符含义示例精确匹配instance10.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{modeidle}[5m]))本质上就是“把所有CPU核的时间加起来减去idle空闲时间剩下的就是被占用的”。写成表达式就是1 - avg by(instance) (rate(node_cpu_seconds_total{modeidle}[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{modeidle}[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{modeidle, cpu0}[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这样除法会先各自聚合成一个值再运算。如果漏掉分子或分母的sumpromeQL会尝试按标签进行向量匹配结果要么报错要么算出N个不同的错误率告警根本没法收敛。4. 聚合操作与进阶函数从数据里提炼结论4.1 rate、irate、increase到底用哪个这是promeQL新手最容易纠结的一组函数。rate()计算区间窗口内每秒钟的平均增长率它会基于窗口的起止值做平滑计算。irate()取区间内最后两个采样点计算瞬时增长率能更敏感地反映突发变化。increase()计算区间内总共增加了多少可以理解为rate() * 窗口秒数的近似值。我的建议是画趋势图、看长时间波动用rate看瞬时突刺、排障时快速定位异常波动用irate看“这半小时总共多了多少请求”用increaseirate有一个反直觉的坑窗口太短且两次采集间隔不稳时它会被两个点的微小抖动放大画出来的图毛刺非常多。比如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{modeidle}[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{modeidle}[5m])) * 100)但要注意topk不是聚合函数它和sum、avg机制完全不同。topk(k, vector)的结果是“保留值最大的k个序列”不会把多个序列合并成一个。因此它只能用于展示和排序不能用于后续运算更不能直接写进告警规则做“当CPU最高那台机器超过阈值”的判断。如果告警要“取最大值”应该用max()max(100 - avg by(instance) (rate(node_cpu_seconds_total{modeidle}[5m])) * 100) 904.4 histogram_quantile分位数延迟计算HTTP延迟、数据库查询耗时分位数通常用Histogram指标存。常见命名格式是_bucket例如http_request_duration_seconds_bucket{le0.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{modeidle}[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的处理是“查询时点采样”。比如step15s它就会在时间轴上每隔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页面里调试一边看数据曲线一边改表达式。曲线一出来你立刻能看出“这步算出来的值是不是符合直觉”。等表达式在图上看着合理了再复制到告警规则里验证阈值这样能省掉很多面向监控面板猜谜的时间。