☰
PromQL实战指南:从基础概念到生产环境常用查询模板
2026/10/16 11:00:41 网站建设 项目流程

告警面板上一排排红点,值班手机震个不停,点开一看又是“高CPU”“高内存”这类老熟人。想写一条优雅的PromQL把根因、趋势、对比一次说清楚,结果要么查不出数据,要么数值离谱,最后只能截图发群里、附一句“容我再看一下”。如果你也经历过这种被PromQL支配的恐惧,这篇内容就是为你准备的。

PromQL是Prometheus内置的查询语言,也是监控数据面板、告警规则、日常排障的底层通用语言。不管是查一台服务器的负载、看一个服务的请求成功率,还是算某个接口的P95延迟,最终都要落到PromQL表达式上。这篇内容不堆砌语法手册,我直接按实际场景梳理了一套常用的PromQL查询模板,包含选择器、聚合运算、rate类函数、histogram_quantile分位数计算,以及一批可以直接复制到Grafana或告警规则里的成品查询。适合刚接触监控、被PromQL绕晕的运维和开发同学,也适合想系统整理自己查询语句的进阶用户。

1. 先理解PromQL的核心概念

写PromQL之前,先把三个基础概念理顺。很多人查不到数据、算错数值,绝大多数都是栽在这几个点上。

1.1 两种向量类型

PromQL表达式返回的结果分为即时向量和区间向量。即时向量是某个时间点上所有时间序列的当前值,比如查“现在的CPU使用率”,返回的就是一个数值快照。区间向量则是某个时间段内所有时间序列的采样值集合,比如查“过去5分钟的CPU使用率”,返回的是这5分钟内每个采集点的值。

两者的使用场景截然不同。Grafana面板上展示趋势图,通常用区间向量配合rate、increase这类函数;告警规则里判断“当前是否超过阈值”,则更常用即时向量。很多人写语句时,直接用区间向量去和数值比较,或者反过来用即时向量去算增长率,自然报错或得到不符合预期的结果。

这里给一个最简单的心法:看到指标名后面带[5m]这种时间范围,就是区间向量;不带的,就是即时向量。函数需要哪种参数,就喂给它哪种。

1.2 标签选择器

指标名加上标签选择器,共同定位一条或一组时间序列。比如node_cpu_seconds_total{cpu="0", mode="idle"},就是筛选出cpu编号为0、模式为idle的那条序列。标签选择器支持四种匹配方式:

  • =:精确匹配,比如job="node-exporter"
  • !=:排除匹配,比如mode!="idle"
  • =~:正则匹配,比如instance=~"192.168.1.*"
  • !~:正则排除,比如mountpoint!~"/var/lib.*"

正则匹配是最容易被忽略但极其有用的工具。比如查所有web节点而不用一个个列IP,查所有5xx状态码而不用把500、501、502挨个写出来,都靠它。但正则语法默认是完整匹配,不是子串匹配,这是最常踩的坑。

1.3 时间偏移

offset关键字可以把查询时间往前挪。比如想对比今天和昨天的同一时刻,就可以在原查询末尾加offset 1d。这在排查“是不是每天固定时段都会出现CPU飙升”这类周期性问题时非常有用。

实际用法很简单:node_cpu_seconds_total{mode="idle"}[5m] offset 1d,就能看到24小时前同一时间点的数据。Grafana里也可以配合$__interval这类变量做动态对比,后面会展开讲。

2. PromQL查询的三大核心操作

理解了基本概念,接下来看写查询表达式时最常用的三类操作:聚合、运算、分组排序。

2.1 聚合操作符

PromQL提供了一组聚合操作符:sum、avg、min、max、count、count_values、stddev、stdvar、topk、bottomk、quantile。最常用的是sum和avg。

sum是把多条时间序列加总,比如把所有核的CPU时间合并成整机的总时间。avg是求平均,常用于把多实例的指标折成一个整体视图。聚合操作通常配合by来指定保留哪些标签维度。比如sum by (instance) (node_cpu_seconds_total),意思是对每个instance分别求和,而不是把所有instance加成一个数。

还有一个点容易混淆:topk和bottomk是特殊的聚合操作符,返回的是原始序列而非聚合后的数值。用topk(5, node_cpu_seconds_total),返回的是数值最大的5条原始序列,而不是一个总和或平均值。所以它适合用来“找出TOP N”,不适合用来做算术。

2.2 二元运算

PromQL支持四则运算、比较运算和逻辑运算。算术运算就是把两条序列逐项计算,比如node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes,每个标签组合各自除一遍。这就是计算内存使用率最基础的形态。

比较运算>、>=、<、<=、==、!=,可以让查询返回布尔值,通常配合告警规则使用。逻辑运算and、or、unless用于组合条件。

这里特别提醒一个向量匹配的细节:当左右两侧的标签集合不完全一致时,PromQL会按标签寻找匹配项。如果两侧标签集不同,可以使用on或ignoring来指定匹配哪些标签。比如sum(node_cpu_seconds_total) by (instance) / sum(node_cpu_seconds_total) by (instance)两侧标签一致没问题,但如果你在后面加一个不带任何标签的常量值,就需要小心匹配规则。

2.3 分组与排序

by和without是聚合操作中控制分组维度的两个关键字。sum by (instance)是按instance分组,sum without (cpu)是除cpu外按所有剩余标签分组。两者结果看似相似,但含义完全不同。

without在指标含有层级标签时更加方便。比如node_cpu_seconds_total带有instance、cpu、mode三个标签,你要统计每个实例的CPU总时间,用sum by (instance)是明确指定保留instance,用sum without (cpu, mode)则是不管cpu和mode两个维度,合并剩下的全部。后者在标签多、不想逐个列的时候,代码更简洁,而且不容易漏标。

排序一般用sort和sort_desc,但在实操中很少直接用于面板,更多是在PromQL表达式中配合topk使用。比如topk(3, sum by (instance) (rate(node_cpu_seconds_total[5m]))),直接找出CPU增长最快的3个实例。

3. 生产环境最常用的PromQL查询集

理论讲再多,不如直接给一批能用的查询。这一节按监控对象分类,整理了一线环境里真正高频使用的PromQL表达式,覆盖CPU、内存、磁盘、网络、服务可用性几个大类。每一条都标注了适用场景,可以直接复制到Grafana里测试。

3.1 CPU监控

CPU使用率是最基础也最容易写错的指标。核心原理是:CPU的总时间在不断累加(counter类型),需要先算增长率,再结合idle模式反推使用率。

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

这条的意思是:取过去5分钟idle模式的CPU时间增长率,求每台机器的平均值,再换算成百分比。为什么用avg?因为一台机器有多核,我们要的是整机视角,而不是每个核一条线。5m的时间窗口决定了波动的平滑程度,窗口越短越灵敏,越长越平滑。

如果想看每个CPU核的单独使用率,去掉avg,保留cpu标签即可:

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

如果想看整体的CPU负载趋势,直接用load1、load5、load15这三个gauge指标:

node_load1 node_load5 node_load15

实践中建议CPU使用率窗口不要低于1分钟,否则告警抖动非常严重,经常会收到“恢复”通知,形成告警疲劳。我一般给告警规则用5分钟窗口。

3.2 内存监控

内存指标在node_exporter里主要是MemTotal、MemAvailable这类gauge值。计算使用率的核心公式:

(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100

这里直接使用MemAvailable而不是MemFree,因为MemFree只是完全空闲的物理内存,MemAvailable还考虑了可回收的缓存和缓冲区,更接近真实可用内存。用MemFree算出来的结果往往偏高,容易制造“内存不足”的假象。

按进程维度统计内存占用,可以用process_resident_memory_bytes:

topk(10, process_resident_memory_bytes)

如果监控的是容器,可以用container_memory_usage_bytes配合namespace过滤。实际使用中还要区分cache和实际使用量,避免因为page cache占据大半内存而误报。

3.3 磁盘监控

磁盘使用率是另一个高频指标。node_exporter提供的node_filesystem_avail_bytes和node_filesystem_size_bytes分别表示可用空间和总空间:

(1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100

注意这里必须过滤mountpoint,否则tmpfs、overlay这些虚拟文件系统会一起计入,算出让人摸不着头脑的结果。生产环境建议按根分区、数据分区分别配置查询,并在告警规则里区分不同目录的阈值。

磁盘IO压力可以用node_disk_read_bytes_total和node_disk_writes_total的增长率来看:

rate(node_disk_read_bytes_total[5m]) rate(node_disk_writes_total[5m])

3.4 网络监控

网络流量用node_network_receive_bytes_total和node_network_transmit_bytes_total:

rate(node_network_receive_bytes_total{device="eth0"}[5m]) rate(node_network_transmit_bytes_total{device="eth0"}[5m])

同样需要过滤device,因为本地的lo回环流量会严重干扰判断。一般建议在抓取侧或者在查询侧把device!="lo"的过滤加上。如果要统一统计所有物理网卡,可以用正则:

sum by (instance) (rate(node_network_receive_bytes_total{device=~"eth.*|ens.*"}[5m]))

3.5 服务与接口监控

服务监控最常用的是HTTP请求指标。假设指标名为http_requests_total,带method、status_code、instance标签:

请求QPS:

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

5xx错误率:

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

成功率则用:

sum by (instance) (rate(http_requests_total{status_code!~"5.."}[5m])) / sum by (instance) (rate(http_requests_total[5m])) * 100

这里用正则5..匹配所有5开头的三位状态码,比逐个枚举500、501、502要优雅很多。实际使用时,如果接口指标带有path标签,还可以进一步按路径维度拆分,直接定位到具体接口。

3.6 业务指标示例

常见业务指标比如订单量、支付成功量、队列积压数,逻辑和HTTP请求类似。假设业务指标为order_total:

每分钟订单量:

increase(order_total[1m])

累计订单量直接查order_total即可,因为counter本身就是累计值。积压类指标通常是gauge,直接查当前值,比如:

queue_size

判断积压是否在持续增长,可以用deriv函数看趋势:

deriv(queue_size[10m])

deriv计算的是区间内曲线的斜率,正值代表持续上升,负值代表下降,这个函数在监控队列、连接池这类指标时非常好用。

4. 两个必会难点:rate与histogram_quantile

4.1 rate、irate与increase的差别

rate、irate、increase三个函数都用于处理counter指标,但计算逻辑不同,选错会导致数值差异巨大。

rate(v range-vector)计算区间内每秒平均增长率。比如rate(node_cpu_seconds_total[5m]),是把5分钟内总增量除以300秒,得到每秒平均增长量。这个值平滑、稳定,适合告警和多日趋势对比。

irate(v range-vector)计算区间内最后两个样本之间的瞬时增长率。它对外部波动更敏感,适合短时间尺度的抖动分析。但在数据点稀疏或者时间范围过短时,irate的结果会波动剧烈,不建议用在告警规则上。

increase(v range-vector)计算区间内的增量,比如increase(http_requests_total[1h])给出的是一小时内的请求总数。这个函数有一个隐藏的坑:Prometheus的采集间隔如果是不固定或者有丢失,increase会根据时间戳外推一个估计值,而不是简单的首尾相减。所以数据跳变、重启counter清零等情况,都会让increase看起来“不正常”。

实际使用我遵循一个原则:长期趋势看rate,短期异常看irate,累计量看increase。三个函数需要处理的是counter类型指标,如果指标是gauge(内存、队列长度这类可增可减的值),不能直接用rate或increase,只能用delta或deriv。

4.2 histogram_quantile与分位数计算

分位数是延迟监控的核心。Prometheus的histogram指标会输出多组_bucket序列,每组对应一个累计计数。histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))是P95延迟最通用的查询写法。

这个查询先对bucket序列求增长率,再用sum by (le)把所有实例的bucket值累加,最后通过histogram_quantile按le顺序插值计算出分位数。关键点有两条。

第一,必须对bucket做rate或increase预处理。直接对原始bucket做分位数计算,得到的是从服务启动以来累计的分布,而不是当前时段的分布。这会让分位数对最新变化毫无反应。

第二,sum聚合时保留le标签是必须的。le是bucket的上界标签,如果聚合时把le丢了,histogram_quantile就没法判断各区间边界,直接报错。

如果只想看某个实例的P95,去掉sum by (le) 即可,保留instance维度:

histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{instance="实例A"}[5m]))

由于histogram_quantile是插值估计,当bucket粒度过粗时结果会偏差较大。比如耗时集中在1ms到100ms之间,但bucket边界只有0.1、1、10这样的稀疏分档,算出来的P95很可能严重失真。所以业务侧埋点的时候,bucket设计要贴合实际分布区间,数据采集端的口径决定了查询端的可信度。

4.3 计算inf与分桶细节

很多人在看histogram原始数据时会发现一个le="+Inf"的桶,它的计数等于所有样本的总数。这个桶是必需的,它标记了累计总个数。对所有le小于等于某个值的桶,计数都是单调递增的。

如果查询结果出现“NaN”,通常是某些bucket的计数为零,插值分母为零导致的。处理办法是在查询中过滤掉空桶,或者检查是否有新的标签组合导致bucket没有匹配上。

5. 常见问题与排障速查表

5.1 报表、面板常见报错与解决

No data是最常见的返回结果。出现这个结果时,我一般按下面顺序排查:

  • 检查TSDB里是否真的有这个指标。在Prometheus界面输入指标名,如果有数据,通常能显示出来。
  • 检查标签名和标签值是否完全一致,特别是有没有多写或少写一个空格。
  • 检查时间范围。刚接入的exporter可能还没采集到完整数据,尤其是查询过去 1 小时的数据时,如果接入时间不足,整个区间都是空的,自然不会出结果。
  • 检查是否用了正则匹配,正则默认是全量匹配,=~"abc"只能匹配完整的abc,不会匹配abc123。

NaN结果通常是0除以0。典型场景是某个新上线的实例还没有流量,rate(http_requests_total[5m])整体为0,再用它做分母,自然得出NaN。

数据全部为0或数值过小,通常是set了错误的时间窗口。比如采集间隔是60秒,你用increase(metric[1m])去算,窗口内最多只有一个样本,其结果要么是缺失,要么是0。这是一个极易踩的坑,建议查询窗口至少是采集间隔的3到4倍。

5.2 查询性能过差的排查思路

在数据量大的集群里,一条糟糕的PromQL能把查询接口拖到超时。最常见的性能杀手是查询范围过大和聚合维度过粗。

比如sum(rate(http_requests_total[10y]))虽然是合法写法,但TSDB需要处理极其庞大的数据范围。虽然Prometheus对于时间范围有压缩机制,但10年的时间跨度没有实际意义,还会引起不必要的开销。

还有一种是标签基数爆炸。比如指标带request_id、user_id这种高基数标签,一个指标动辄几百万条序列,每次查询都要扫描全量序列,性能自然不可控。这类场景需要从埋点端解决,而不是靠查询端优化。

查询侧能做的优化有三个方向:缩小时间范围、减少标签匹配的复杂度、尽量使用sum/avg等聚合操作把数据量降下来再参与运算。把多个实例的明细序列先聚合成一个,往往能让查询速度快一个量级。

5.3 我的排障心法

排查PromQL问题,我最常用的方法是分步拆解。先查裸指标,确认数据是否存在;再加标签过滤,确认筛选条件是否正确;然后加函数,确认计算逻辑是否合理;最后加聚合和运算,确认最终输出是否符合预期。每一步都在Prometheus的查询框里先跑一下,确定无误后再挪到Grafana或告警规则里。

比如要排查“内存使用率为什么显示为0”,我先查node_memory_MemAvailable_bytes看有没有数据,再查node_memory_MemTotal_bytes看数值是否为0,确认分母没问题后再套除法公式。这样拆开来,每次只验证一个变量,比盯着整条表达式瞎猜要快得多。

另一个小技巧是善用Prometheus自带的explain注释,虽然它不提供最终答案,但每个表达式都可以用#的方式加注释方便自己在多行查询里理清思路。用Grafana编辑查询时,也建议在查询语句附近留一行注释,标记这条面板的表达意图,对后期维护帮助很大。

6. 告警规则里的PromQL实战

PromQL不仅用于可视化面板,在告警规则里同样是最核心的部分。告警规则的PromQL写法和面板查询有一个重要区别:规则评估要求查询结果在评估时刻是有效的,所以通常会使用即时向量而不是区间向量做阈值判断。

比如“CPU使用率超过90%并持续5分钟”的告警规则:

rules: - alert: HighCPUUsage expr: > 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90

注意这里rate的时间窗口和规则评估间隔之间的关系。如果评估间隔是1分钟,5分钟窗口意味着至少要5个窗口才能覆盖完整的增长趋势,所以告警会有延迟。想要更灵敏的告警,可以适当缩小窗口,但也要承受更多的抖动。

另外告警规则里经常配合for字段来过滤瞬时抖动。比如expr已经满足条件,但for: 5m会要求这个条件持续满足5分钟后才触发告警,这是比单纯缩短查询窗口更优雅的方案。

多条件告警可以用and连接。比如同时要求CPU高且负载高:

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

这里load1是gauge类型,可以直接和数值比较,而CPU使用率必须先计算出百分比再比较,两者逻辑完全一致。用and连接后,只有当两条条件在同一时刻都满足时,才会触发告警,可以有效减少误报。

7. 进阶技巧与常用模板

基础查询能跑通之后,还有几个提升效率的进阶技巧值得掌握。

7.1 Grafana变量联动

Grafana面板中常用变量来动态切换实例或标签。在查询中使用$instance、$interval这类变量,面板就可以一个模板复用多台机器。

典型的写法是把实例列表做成变量:

100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle", instance="$instance"}[$interval])))

这样切换下拉框就能在不同实例之间切换,不需要为每台机器单独写一条查询。变量的查询通常用label_values来获取:

label_values(node_cpu_seconds_total, instance)

7.2 同比环比查询

对比今天和昨天的同一时段,是排障最常用的需求。PromQL的offset可以方便地实现:

node_memory_MemAvailable_bytes - node_memory_MemAvailable_bytes offset 1d

这个表达式的含义是今天的可用内存减去昨天的可用内存,正数代表可用内存增加,负数代表减少。Grafana里可以把当前值和offset值放在同一张图中对比,并用#区分颜色。

7.3 多实例中位数与异常识别

当集群有几十台实例时,单看每台实例的曲线很难发现异常。可以先用quantile聚合出整体分布:

quantile(0.5, sum by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])))

再对每个实例计算偏离程度,用最新值减去中位数来识别离群实例。这类查询虽然复杂,但在大集群排障时很实用。

7.4 空值填充

有些监控场景下指标为零才意味着故障,但PromQL默认不显示零值,面板上就会出现断线。这种情况下可以用clamp_min或or操作符把零值带入查询,比如:

rate(http_requests_total[5m]) or on() vector(0)

这个表达式保证在没有数据时输出0,避免Grafana面板出现空洞。

8. 从查询到监控体系的完整视角

PromQL承载的是监控体系里“查询”这一层,但它与采集端、告警端是强耦合的。查询写得再漂亮,如果采集指标的口径不对,算出来的结果依然不可信,这就是我为什么反复强调要先理解采集端指标语义的原因。

比如counter和gauge的区分,直接决定了能否用rate,而埋点时的类型错误在查询端是无解的。再比如labels的设计,高基数的标签不仅会让查询变慢,还会让PromeQL的每次扫描都得翻遍全量序列。采集端设计得好,查询端会非常轻松;采集端设计混乱,查询端再优化也是治标不治本。

从实践经验来看,一个新项目接入监控时,最值得花时间的不是先把Grafana面板做漂亮,而是先梳理好指标命名规范、标签规范和类型规范。也就说是,先想清楚需要看哪些核心指标,每个指标是什么类型,以及需要哪些维度。这样后续每一块面板、每一条告警规则的PromQL,都是水到渠成的事。

这些内容只是监控体系建设的一个切面,PromQL本身的知识点远比这更丰富,但真正高频使用、真正解决问题的,往往就是上面这些。把常用查询模板整理好,把每个函数背后的计算逻辑弄明白,再配合一套规范的采集端设计,监控这件事就能省下大量排障时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询