告警面板上一排排红点,值班手机震个不停,点开一看又是“高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本身的知识点远比这更丰富,但真正高频使用、真正解决问题的,往往就是上面这些。把常用查询模板整理好,把每个函数背后的计算逻辑弄明白,再配合一套规范的采集端设计,监控这件事就能省下大量排障时间。