PromQL实战指南:从基础概念到生产环境常用查询模板

发布时间:2026/10/16 11:00:45
PromQL实战指南:从基础概念到生产环境常用查询模板
告警面板上一排排红点值班手机震个不停点开一看又是“高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{cpu0, modeidle}就是筛选出cpu编号为0、模式为idle的那条序列。标签选择器支持四种匹配方式精确匹配比如jobnode-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{modeidle}[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{modeidle}[5m])) * 100)这条的意思是取过去5分钟idle模式的CPU时间增长率求每台机器的平均值再换算成百分比。为什么用avg因为一台机器有多核我们要的是整机视角而不是每个核一条线。5m的时间窗口决定了波动的平滑程度窗口越短越灵敏越长越平滑。如果想看每个CPU核的单独使用率去掉avg保留cpu标签即可100 - (rate(node_cpu_seconds_total{modeidle}[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_bytestopk(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_totalrate(node_network_receive_bytes_total{deviceeth0}[5m]) rate(node_network_transmit_bytes_total{deviceeth0}[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标签请求QPSsum 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_quantile4.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原始数据时会发现一个leInf的桶它的计数等于所有样本的总数。这个桶是必需的它标记了累计总个数。对所有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{modeidle}[5m])) * 100) 90注意这里rate的时间窗口和规则评估间隔之间的关系。如果评估间隔是1分钟5分钟窗口意味着至少要5个窗口才能覆盖完整的增长趋势所以告警会有延迟。想要更灵敏的告警可以适当缩小窗口但也要承受更多的抖动。另外告警规则里经常配合for字段来过滤瞬时抖动。比如expr已经满足条件但for: 5m会要求这个条件持续满足5分钟后才触发告警这是比单纯缩短查询窗口更优雅的方案。多条件告警可以用and连接。比如同时要求CPU高且负载高( 100 - (avg by (instance) (rate(node_cpu_seconds_total{modeidle}[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{modeidle, 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{modeidle}[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本身的知识点远比这更丰富但真正高频使用、真正解决问题的往往就是上面这些。把常用查询模板整理好把每个函数背后的计算逻辑弄明白再配合一套规范的采集端设计监控这件事就能省下大量排障时间。