PromQL实战指南:从时间序列思维到监控查询

发布时间:2026/10/7 5:17:05
PromQL实战指南:从时间序列思维到监控查询 这套Prometheus学习笔记已经写到第3章了。前两章我们把架构、部署、抓取链路全部跑通Targets一片绿Grafana也接上了。但几乎每个团队走到这一步都会撞上同一个坎——数据都躺在Prometheus里面板上却只能拉个最简单的折线图想查“过去5分钟CPU使用率超过80%的实例有哪些”这种问题突然发现自己不会说监控系统的“方言”了。这门方言就是PromQL。PromQL是Prometheus的原生查询语言我见过太多人低估它。有人觉得它和SQL差不多有人以为就是Grafana面板上的点点点还有人把它当成一堆函数死记硬背。这些想法都会在真实排障时碰壁。我个人的体会是PromQL本身语法不复杂但它背后是一套完全不同于传统数据库的时间序列思维一旦把数据模型和查询语义想通后面写告警、做报表、排查高延迟都顺手很多。这篇笔记不打算按官方文档逐条翻译我会从实际使用角度把PromQL的底层逻辑、高频操作和容易翻车的细节一次讲清楚。1. 为什么说PromQL是监控系统里最值得投资的那门“外语”1.1 从现象说起数据有了但不会“问问题”很多朋友部署完Prometheus之后第一件事是打开Grafana找现成Dashboard。开源社区里模板确实多import进来图表就出来了看得眼花缭乱。但模板是别人的思路一旦指标名对不上、标签含义不一样图表全是No data。这时候你想改一改查询一看面板里的PromQL表达式就头大。更常见的是告警场景。默认部署里只有少数几个自带告警规则真正到业务上你需要写“订单接口P99延迟连续5分钟超过500ms”“某台机器磁盘再有4小时就满了”这样的规则。这些全部要用PromQL表达。换句话说PromQL不是锦上添花的进阶功能而是从“能用监控”到“用好监控”的分水岭。1.2 消除对PromQL的刻板印象它不是SQL也不只是函数库第一次接触PromQL的人容易拿它和SQL比较。SQL操作的是关系表行和列有清晰的结构PromQL操作的是时间序列——每个序列由指标名加一组标签唯一标识每个点带一个时间戳和一个浮点数值。SQL里你用WHERE过滤行PromQL里你用选择器过滤序列然后用时间窗口切出数据点。还有一个更隐蔽的区别SQL的查询结果是一次性快照PromQL的查询结果天然是“随时间变化的”。同一个查询在Grafana里选择最近15分钟和最近24小时你会看到不同形态的数据这不是查询写错了而是因为PromQL本身就是按时间维度组织的。所以我说它是“外语”——语法可以速成思维方式需要一点适应过程。2. 数据模型先行metric、标签与样本是怎么决定查询结果的2.1 metric name只是入口标签才是查询的维度在Prometheus里一条时间序列的基本结构是这样http_request_total{methodPOST, handler/api/order, instance10.0.1.7:9100} 1635678823000 - 1024http_request_total是metric name后面花括号里是标签集合 时间戳 - 数值表示一个样本。metric name加标签集合才能唯一定位一条时间序列标签不同就是不同序列。这个模型的直接推论是标签就是你的查询维度。你想按接口看流量就按handler分组你想按机器看负载就按instance分组你想知道某个Pod在哪个节点上节点信息如果不是标签那PromQL天然查不到。所以采集端在暴露指标时把什么信息放进标签基本决定了未来你能从监控里挖出什么。我遇到过不少团队把多个维度拼进metric name比如http_request_total_method_post_handler_order这在Prometheus里是反模式。正确做法是保持metric name不变用标签区分维度。因为PromQL里聚合、分组、关联全靠标签metric name唯一的作用是选中一组序列。2.2 job、instance这些“送上门”的标签配置Prometheus抓取target时抓取器会自动给每个样本附带一些标签。job来自 scrape_config 里的 job_nameinstance默认取 target 的__address__通常是ip:port。这两个自动标签在写查询时经常用到也经常被忽略。举个例子你在prometheus.yml里配了scrape_configs: - job_name: node static_configs: - targets: [192.168.1.10:9100, 192.168.1.11:9100]那么查up{jobnode}就会返回每个target的存活状态1代表抓取正常0代表失败。instance标签的默认值就是192.168.1.10:9100这样的地址。如果你在业务指标里还想区分机房、环境可以在采集端通过标签重写relabel_configs加入env、region这类标签。这些在PromQL里都可以直接用于过滤和分组。2.3 标签基数查询性能的隐形天花板数据模型带来一个所有Prometheus使用者都会面对的问题——标签基数cardinality。如果一个标签的取值数量涨到无限比如把用户ID、订单号放进标签那么时间序列数量就会爆炸。Prometheus是单机内存索引的架构序列数到几千万之后即使查询再简单也会拖慢整个实例。我在实践中的底线是标签取值要“有界”。method有十几种取值没问题user_id可能有几百万种绝对不能作为标签。PromQL写得好不好很多时候也体现在这——如果你发现一条查询要scan几百万条序列才能算出一个数字那问题往往不在查询而在指标设计。3. 选择器与向量让数据点精确落位3.1 瞬时向量与区间向量两种完全不同的查询请求PromQL的查询返回结果分为两种瞬时向量instant vector和区间向量range vector。这一节必须弄清楚因为绝大多数函数对输入类型有严格限制。瞬时向量是某个时间点上的所有序列快照比如http_request_total不带时间范围只返回最新样本。区间向量是某个时间窗口内的样本集合写法是在选择器后面加方括号比如http_request_total[5m]返回最近5分钟每个序列的所有样本点。很多人一开始不理解为啥要区分。我的理解是瞬时向量适合计算当前状态比如某台机器现在内存还剩多少区间向量适合计算变化过程比如每秒钟请求量涨了多少。速率类函数rate()、increase()只能接受区间向量而比较、算术运算通常处理瞬时向量。如果你在Grafana里直接写rate(http_request_total[5m])返回的是瞬时向量——每个序列在5分钟窗口内的每秒增长率这个结果可以在图上画出来如果你只写http_request_total[5m]Grafana会告诉你类型不匹配或直接画不出理想图形。3.2 标签匹配的四种操作符与正则细节选择器支持四种匹配方式操作符含义示例精确匹配node_cpu_seconds_total{cpu0}!反向精确匹配node_cpu_seconds_total{cpu!0}~正则匹配http_request_total{handler~/api/.*}!~正则反向匹配http_request_total{handler!~/static/.*}正则匹配是实战高频操作这里有两个容易翻车的点。第一~如果没写锚定符就是子串匹配。handler~order会匹配到/api/order、/v1/order/list等所有包含order的路径。如果你只想要/api/order这一个路径要写handler~/api/order$。我建议在正则里习惯性加上^和$避免匹配范围超出预期。第二正则表达式中需要匹配点号.、斜杠/时注意点号在正则里通配任意字符要写成\.。还有一个冷门但好用的写法{__name__~.*_total}。可以直接用metric name做正则匹配一次选出命名有规律的一批指标。这在刚开始探索一个exporter有哪些指标时特别有用。3.3 offset与时间修饰符跨时间对比的利器查询里加offset可以回看过去的数据比如http_request_total[5m] offset 1h含义是“1小时前那个时间点的最近5分钟窗口”。这在做同比、环比对比时非常实用。我经常在告警恢复判断里看到这种需求当前错误率上升但需要知道是不是和1小时前一样——用offset直接把两条曲线叠在同一个图上视觉上非常直观。修饰符可以指定一个精确的UNIX时间戳来计算瞬时向量比如查看某个历史时刻的CPU使用率。这个用法在日常排障中不算频繁但在复盘事故时偶尔能救命。4. 操作符、聚合与向量匹配让指标从“原始数据”变成“答案”4.1 算术、比较、逻辑三步组合完成指标运算PromQL对瞬时向量支持四类运算符算术 - * / % ^比较 ! 逻辑and or unless聚合sum avg min max ...算术运算符的规则是标量和向量运算时标量会应用到每个样本两个向量运算时按相同标签集合匹配后逐样本计算。举一个最常见的例子内存使用率(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100这个查询里MemAvailable_bytes和MemTotal_bytes是两条序列标签集合相同都是instance那一套所以逐样本相除得到的是每台机器的可用内存比例。比较运算符有个特别容易忽略的修饰符bool。默认情况下http_request_total 1000会过滤掉不满足条件的序列只返回大于1000的那些加上bool之后返回结果变成0或10代表不满足1代表满足。告警规则里经常需要这种布尔输出做多条件组合判断时尤其有用。逻辑运算符里我要特别提一下unless。A unless B返回只在A中出现、不在B中出现的序列。比如你想找出“正在运行但没有通过健康检查的容器”可以先查出所有容器序列再用unless去掉健康检查为1的容器序列一步搞定。4.2 聚合的by/without决定结果保留哪些维度PromQL的聚合函数很多sum、avg、min、max、count、stddev、topk、quantile等。聚合之后标签维度会被压缩这时要用by或without控制保留哪些标签。这两者经常让人搞混我自己的理解方式是by (label)表示“按哪些标签分组聚合”结果保留这些标签。without (label)表示“把哪些标签拿掉之后聚合”结果保留其余所有标签。举例来说node_exporter的CPU指标是每个CPU核一条序列带cpu0、cpu1标签。如果我想看整机CPU空闲率可以用without (cpu)把cpu维度抹掉也可以用by (instance)只保留实例维度。两者在这个场景下结果相同。但如果序列里还有mode、job等其他标签用without(cpu)会保留它们by(instance)则只留实例。初学者建议时刻问自己一句话我最终要按什么维度看数据要保留什么就写by。告警规则里如果场景单一by更明确Grafana变量联动查询时without反而更省心因为不用列出一大堆要保留的标签。4.3 向量匹配把两个指标做关联的on/ignoring/group_left算术运算发生在两个向量之间时PromQL默认要求“完全匹配”——两边序列的标签集合一模一样才配对。但实际场景里两个指标往往不是完美对齐的。比如你想算http_request_total / 当前副本数副本数来自另一个指标kube_deployment_replicas标签只有namespace、deployment没有instance那两边就匹配不上。这时需要用on()或ignoring()指定匹配维度sum by(namespace, deployment) (rate(http_request_total[5m])) / on(namespace, deployment) group_left kube_deployment_spec_replicason(namespace, deployment)表示只按这两个标签配对其他标签忽略。group_left允许左侧被除数的向量多出一些标签这样每个deployment的请求速率可以除以它的期望副本数得到单副本QPS的估算值。group_right则反过来。这个功能在指标关联元数据时经常用到。比如节点指标里只有instanceip:9100而告警通知需要机架ID、所属业务这类信息来自CMDB接口生成的指标标签里没有instance这时就可以用on()加group_left把两个序列关联起来。5. rate、irate、histogram_quantile与predict_linear监控查询的四个高频武器5.1 rate与iratecounter指标的正确打开方式Prometheus的指标类型分为counter只增不减的计数器和gauge可增可减的测量值。CPU总时长、请求总数、字节数都是counter直接查询它们没有意义——你关心的是增长速度而不是累计值。计算增长速度的标准函数是rate。rate(http_request_total[5m])的含义是在5分钟窗口内计算每秒请求数的平均增长率。Prometheus在内部会处理counter重置比如服务重启导致计数归零的问题它会识别出“掉到0再重新涨”的模式并把这段增量计入速率。这里有一个我反复强调的经验rate窗口至少要是抓取间隔的4到5倍。如果你的抓取间隔是15秒窗口选1分钟4倍勉强可用选5分钟更平滑。窗口太短曲线跳变严重看起来像锯齿窗口太长响应太迟钝告警可能错过最佳介入时机。irate是另一个高频选择它只计算区间内最后两个样本之间的瞬时速率不受窗口内历史数据影响。irate对突刺更敏感适合短时间窗口观察瞬时流量但它对样本丢失很敏感两个样本之间一旦断了结果可能直接跳变。我的习惯是仪表盘观察用irate配短窗口1分钟告警规则用rate配长窗口5分钟前者灵敏、后者稳定。5.2 histogram_quantile从桶到分位数的估算在Web服务监控里聚合延迟比平均延迟更有价值因为平均延迟掩盖了长尾。Prometheus的histogram类型通过累计桶计数实现分位数估算。假设指标是http_request_duration_seconds_bucket每个桶带le标签le表示“小于等于多少秒”。要计算P99延迟常见写法histogram_quantile(0.99, sum by(le) (rate(http_request_duration_seconds_bucket[5m])))先对每个桶做rate把累计计数转换成每秒增量再用sum by(le)合并所有实例最后histogram_quantile在桶边界之间做线性插值估算。有两个坑必须留意。第一histogram_quantile的结果是估算值桶越密越准。如果你的桶配置只有三四个算出来的P99可能误差很大。第二le桶必须是按边界从小到大排列的累积计数如果你自己的exporter返回的桶顺序是乱的这个函数会直接报错。实践中我建议分桶至少覆盖0.005、0.01、0.025、0.05、0.1、0.25、0.5、1、2.5、5、10这个粒度足够支撑大多数Web延迟场景。5.3 predict_linear与其他实用函数容量规划场景里predict_linear是我的首选。它基于区间向量做线性回归预测未来某个时刻的值。例如磁盘使用率预测predict_linear(node_filesystem_avail_bytes{mountpoint/}[6h], 4*3600) 0含义是基于过去6小时的变化趋势预测4小时后的剩余磁盘空间是否小于0。这种告警比“磁盘使用率超过90%”要提前得多能赶在故障前处理。另外还有一组常用函数值得记住increase窗口内增量、deltagauge指标窗口内差值、sort/sort_desc排序、clamp_max限制上限、abs绝对值、time()当前UNIX时间戳可用于计算运行时长。这些函数单个很好理解组合起来能处理不少“脏活”。6. 三个实战查询 一条告警规则从零拼装一条查询6.1 CPU使用率与内存使用率CPU使用率的经典写法100 - (avg by(instance) (rate(node_cpu_seconds_total{modeidle}[5m])) * 100)拆解一下node_cpu_seconds_total是每个核在不同模式下的累计时间modeidle选中空闲模式rate([5m])算出每秒空闲比例乘100得到空闲百分比avg by(instance)把每个核平均到实例层面最后用100减得到CPU使用率。为什么不直接sum非idle模式因为非idle模式有user、system、iowait、steal等一堆全部sum起来容易重复而且各种模式的语义在不同exporter版本上有差异。从idle反推是最稳健的做法。内存使用率有个常见的坑新手用MemFree_bytes算可用内存会把cache和buffer全当成已用。正确做法是用MemAvailable_bytes(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100MemAvailable是内核考虑页缓存可回收性之后给出的真实可用内存估计用它算出来的使用率才和free命令里显示的结果对得上。6.2 HTTP QPS与延迟P99如果你的服务暴露了http_request_totalcounter和http_request_duration_seconds_buckethistogram这两组指标QPS和P99延迟可以这样写sum(rate(http_request_total[1m]))histogram_quantile(0.99, sum by(le, handler) (rate(http_request_duration_seconds_bucket[5m])))第二句比前面多了一个handler维度是因为我想按接口分别看P99。把handler换成instance就是按实例看去掉by(le, handler)就是全局限P99。聚合粒度完全由业务决策决定这正是PromQL灵活性的体现。6.3 一条告警规则从写到验证以磁盘告警为例完整的prometheus告警规则片段groups: - name: disk-alerts rules: - alert: DiskWillFillIn4h expr: predict_linear(node_filesystem_avail_bytes{mountpoint/}[6h], 4*3600) 0 for: 10m labels: severity: warning annotations: summary: 磁盘预计4小时内写满 description: 实例 {{ $labels.instance }} 根分区剩余空间预计在4小时内耗尽注意几个细节expr里窗口6h不要比告警周期短太多否则趋势不稳定for: 10m是持续10分钟都满足条件才触发避免单次抖动误报{{ $labels.instance }}是告警模板变量Prometheus会替换成实际标签值。写完规则之后可以用promtool check rules做语法检查然后reload Prometheus配置。我强烈建议先在Grafana的Explore里跑一遍这条表达式确认当前值不是负数再把它写进规则。很多人跳过验证步骤结果告警规则一加载就直接触发因为表达式里有个不起眼的语法错误被默认值兜住了。7. PromQL排错现场我踩过的坑和修复方法7.1 查询返回空结果PromQL查询为空90%的情况不是Prometheus出问题而是表达式写得不到位。最常见的五种原因标签名或标签值拼错。比如modeidel差一个字母结果就是空。正则没有锚定匹配范围过宽或过窄。区间向量窗口小于抓取间隔。比如抓取间隔15秒你写rate(metric[10s])窗口内可能只有一个或零个样本rate直接返回空或者NaN。指标名不对。某些exporter的指标名在不同版本之间改过先到/metrics端点确认实际名字。数据断档。target挂了或者被重启序列样本里有空洞窗口内的数据点不够函数可能放弃计算。排查方法是用Grafana Explore逐步拆解先查metric看有没有数据再加标签选择器再加窗口函数。每加一层都确认当前返回结果非空问题通常就出在最后加的那一层。7.2 数值跳变、速率趋近于零等反直觉现象如果rate曲线突然掉到0又突然跳回来多半是样本对齐和外推导致的。Prometheus对区间向量计算速率时如果窗口两端的样本不在精确的边界上会做线性外推。样本越稀疏外推误差越大。让窗口长度尽可能覆盖4到5个抓取周期能显著减少这种毛刺。还有一类的反直觉现象是counter明明在涨increase却算出来的值和实际对不上。原因在于increase也是基于rate乘以窗口时长计算的它的结果是在样本区间上外推出来的不是精确的窗口内差值。如果有任何一次counter重置发生在窗口边缘误差会被放大。解决思路还是那句话——把窗口放宽。7.3 与Grafana联调时的几个注意点热词里提到prometheus grafana安装部署这里多说几句联调细节。Grafana面板里写PromQL时有两个隐藏变量值得用起来$__rate_interval和$__interval。前者是Grafana根据当前面板时间范围和抓取间隔自动算出的推荐rate窗口直接写rate(metric[$__rate_interval])可以避免我前面说的“窗口太短”问题。后者是当前面板的计算步长常用于offset对比。另一个常用技巧是在Grafana的Explore页面里可以随意修改查询的时间范围用这个功能把时间范围拉到1小时、6小时、24小时分别观察曲线形态。同一个PromQL在不同时间范围下的表现差异很大特别是histogram_quantile这类带统计性质的表达式样本量不足时估算结果会很不稳定。最后提醒一点query的返回样本数控制在合理范围。Grafana和Prometheus之间的查询如果一次拉几十万条序列前端渲染会卡死。遇到这种场景先sum或topk压缩维度再交给图表。8. 给初学者的最后建议PromQL的学习曲线其实不在语法而是习惯用“序列、标签、维度”的方式思考监控。我的建议是不要从头到尾读一遍官方文档直接拿自己环境里的exporter开刀从复制一条查询开始逐层拆解先去掉聚合函数看原始序列再加上过滤条件最后套上时间窗口和函数。在Explore里反复改、反复看曲线的过程比任何教程都有效。如果非要说一个最容易提升查询水平的小技巧那就是坚持给所有标签写正则时加锚定、给所有rate窗口留足时间跨度。这两件事做对了PromQL一半的坑你已经不用踩了。