
PQL这个词第一次听到的人十有八九会把它和SQL搞混或者干脆以为是某个数据库厂商的私有方言。我在带新人的时候被问得最多的一个问题就是“PQL到底是个啥跟SQL有啥区别我学它干嘛”其实PQL全称是Prometheus Query Language是Prometheus这套监控体系里用来查询时序数据的语言。它跟SQL最大的不同在于SQL面向的是关系型表结构而PQL面向的是时间序列——也就是带时间戳的一串数值。你写PQL的时候脑子里想的不是“从哪张表里select什么字段”而是“我要在哪个指标上、按什么维度、取哪个时间窗口的数据”。这篇内容我打算写给两类人看一类是刚接触监控体系、需要快速上手写查询的初学者另一类是用了一段时间PQL但总觉得写出来的查询又慢又乱、想系统梳理一下写法的人。我会从最基础的查询结构讲起把指标选择、标签过滤、时间窗口、聚合运算这几个核心环节拆开揉碎再补上几个我实际踩过的坑和优化技巧。五分钟上手不是夸张但五分钟之后你得知道往哪个方向继续练。1. 先搞清楚PQL在查什么时序数据的结构决定了写法1.1 一条时间序列长什么样要写好PQL第一步不是背语法而是理解它操作的对象。Prometheus里的每一条时间序列由两部分构成一个指标名称metric name加上一组标签label。指标名称描述“这是什么”标签描述“这个数据的维度特征”。比如一个典型的指标长这样http_requests_total{methodPOST, handler/api/orders, status200}这条序列的含义是HTTP请求总数限定方法为POST、路径为/api/orders、状态码为200。注意这里的http_requests_total是一个累加型指标counter它的值只会增长不会减少。这一点非常关键因为PQL里大量的查询都是围绕counter的增长率来做的而不是直接读它的绝对值。你可以把一条时间序列想象成一条心电图横轴是时间纵轴是数值每个采样点间隔固定默认15秒。PQL做的事情就是在这条曲线上做切片、做运算、做聚合。理解了这一点你就能明白为什么PQL里到处是rate()、increase()、[5m]这类东西——它们都是在处理“一段时间内的变化”。1.2 指标类型决定了你该用什么函数Prometheus有四类指标Counter计数器、Gauge仪表盘、Histogram直方图、Summary摘要。初学者最容易犯的错就是拿处理Counter的方式去处理Gauge或者反过来。我见过有人对一个表示当前内存占用的Gauge指标套rate()结果查出来一堆莫名其妙的负数和小数。记住一个简单的判断原则只增不减的用Counter可增可减的用Gauge。Counter适合统计请求数、错误数、处理字节数这类累计量查询时几乎都要配rate()或increase()Gauge适合统计CPU使用率、内存占用、队列长度这类瞬时值直接查或者配avg_over_time()就行。Histogram和Summary稍微复杂一些它们会自动生成带_bucket、_sum、_count后缀的衍生序列后面讲分位数的时候会细说。提示如果你不确定某个指标是什么类型可以在查询框里直接输入指标名回车看返回的数值是不是单调递增的。单调递增基本就是Counter。2. 从零写第一条查询选择器与标签匹配的实操细节2.1 指标选择器的基本写法最简单的PQL查询就是一个指标名node_cpu_seconds_total回车之后你会看到一堆序列因为每台机器、每个CPU核心、每种模式都会生成一条。这时候就需要标签过滤来缩小范围。标签匹配支持四种运算符运算符含义示例精确等于modeidle!不等于mode!idle~正则匹配mode~idle|user!~正则不匹配mode!~idle|user组合起来就是node_cpu_seconds_total{modeidle, instance10.0.0.1:9100}这里有个初学者常忽略的点标签匹配是精确到序列级别的。你写的每一个标签条件都会参与序列的筛选条件越多筛得越细。但反过来条件写太多也可能一条都匹配不到因为标签名或标签值拼错一个字符就全落空。我的习惯是先用宽条件查出来看清楚实际有哪些标签和值再逐步收紧。2.2 正则匹配里的坑锚定与转义~用的是RE2正则语法默认是部分匹配不是全匹配。也就是说mode~idle会匹配到idle但如果有个值是non-idle它也会被匹配上因为包含子串。要精确匹配必须加锚定mode~^idle$。这个坑我在早期项目中踩过当时查CPU空闲率结果把iowait相关的某些值也带进来了数据对不上排查了半天。另外正则里的特殊字符要转义比如路径标签里带.或得写成\.和\。还有一点正则匹配的性能开销比精确匹配大如果标签值是有限枚举能用就别用~。在大规模集群上一个写得随意的正则可能让查询慢上好几秒。2.3 时间窗口方括号里的数字不是随便填的PQL里凡是涉及“一段时间”的运算都要用方括号指定范围比如[5m]表示最近5分钟。这个窗口的选择直接决定了查询结果的平滑程度和灵敏度。窗口太短曲线抖动剧烈看不出趋势窗口太长反应迟钝故障已经恢复了曲线还没降下来。我的经验是窗口至少要是采集间隔的4倍。默认采集间隔15秒那窗口最小填1m实际用5m比较稳妥。对于变化缓慢的指标比如磁盘使用率可以用1h甚至更长对于需要快速告警的指标比如错误率突增1m到5m之间比较合适。还有一点rate()要求窗口内至少有两个采样点如果窗口设得比采集间隔还短会直接返回空值。3. 让查询真正有用rate、聚合与分组的三板斧3.1 rate()为什么是Counter查询的灵魂前面说过Counter只增不减直接读它的值没有意义——你看到的是从进程启动到现在的累计量。真正有用的是“单位时间内的增长速率”这就是rate()干的事。它的计算逻辑是取窗口内第一个和最后一个采样点算差值再除以窗口秒数。rate(http_requests_total{status500}[5m])这条查询返回的是“最近5分钟内每秒产生多少个500错误”。注意单位是每秒不是每分钟。如果你想要每分钟的数量用increase()它等价于rate()乘以窗口秒数。这两个函数经常被混用记住看速率用rate看总量用increase。还有一个细节rate()会自动处理counter重置的情况比如进程重启导致计数归零它会把重置点之前和之后的数据分段计算再拼接。这个特性是PQL内置的不需要你手动处理但你要知道它存在否则看到重启后的曲线会觉得奇怪。3.2 聚合运算sum、avg、max该怎么选单条序列的查询往往不够用实际场景里我们关心的是“所有实例加起来是多少”或者“平均值是多少”。PQL提供了一组聚合函数最常用的几个sum()求和适合统计总请求量、总错误数avg()求平均适合看整体水位max()/min()取极值适合找最忙或最闲的实例count()计数适合统计实例数量聚合的时候通常要配合by或without来指定分组维度。比如sum(rate(http_requests_total[5m])) by (handler)这条查询的意思是先算每个序列的请求速率再按handler标签分组求和。结果就是每个接口路径的总QPS。by后面跟的是你想保留的标签其余标签在聚合时被丢弃。without则相反指定要丢弃的标签保留其余所有。这里有个容易搞混的地方by和without不能同时用选一个就行。我的习惯是用by因为显式列出保留维度更清晰不容易漏掉关键标签。但如果你有大量动态标签比如pod名用without把高基数标签排除掉会更省事。3.3 分组维度选错结果全废聚合的分组维度直接决定了你看到的数据形态。举个真实场景你想看每个服务的错误率如果按instance分组你会得到几十上百条曲线根本没法看如果按service分组就能得到每个服务一条线一目了然。但如果你漏掉了service标签只按status分组那所有服务的错误就混在一起了。选分组维度的原则是按你决策的粒度来分。你要定位到哪个服务出问题就按服务分你要定位到哪台机器就按实例分。维度太细信息过载维度太粗定位不到根因。我一般会先按粗粒度看全局发现异常后再下钻到细粒度。4. 进阶写法分位数、子查询与避免常见性能陷阱4.1 histogram_quantile算P99的正确姿势延迟类指标通常用Histogram类型记录它会自动生成_bucket序列每个bucket是一个数值上限le标签记录的是小于等于该上限的请求数。要算P99延迟用histogram_quantile()histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, handler))这里有几个关键点。第一rate()要作用在_bucket上不是_sum或_count。第二sum聚合时必须保留le标签否则分位数算不出来。第三histogram_quantile的第二个参数是聚合后的bucket序列不是原始序列。这三条任何一条写错结果要么是NaN要么完全失真。还有一个常见误区直接把多个实例的P99再求平均这是错的。分位数不能做算术平均必须先聚合bucket再算分位数。我见过有团队用avg(histogram_quantile(...))算出来的值比真实P99低一大截导致告警阈值形同虚设。4.2 子查询当你要对“速率”再做一次运算有时候你需要对一段时间内的rate结果再做聚合比如“最近一小时内每5分钟的QPS峰值是多少”。这时候就要用子查询max_over_time(rate(http_requests_total[5m])[1h:5m])方括号里的1h:5m表示在1小时范围内以5分钟为步长取值。子查询的语法是[range:step]range是总时间范围step是采样步长。step不写的话默认用全局的evaluation interval。子查询的性能开销比普通查询大因为它要在内存里展开多个时间点的计算。能不用尽量不用如果非用不可把range和step控制得尽量小。另外子查询嵌套层级别太深两层以上基本就该考虑换个思路了比如用Recording Rule预计算。4.3 高基数标签是查询变慢的头号元凶PQL查询慢十有八九是因为标签基数太高。什么叫高基数就是一个标签有成千上万个不同的值。典型的高基数标签包括pod_name、request_id、user_id、url带参数的完整路径。这些标签会让序列数量爆炸查询时扫描的数据量成倍增长。我的处理原则是查询时尽量不碰高基数标签或者在聚合阶段把它们丢掉。比如查接口延迟不要按完整URL分组按路由模板如/api/orders/:id分组就够了。如果指标本身就把高基数信息打进了标签那在采集端就该做处理而不是等到查询时再补救。注意{__name__~.}这种全量匹配的查询在生产环境基本等于自杀它会扫描所有序列。除非你在做调试否则永远不要这么写。5. 我踩过的几个坑和对应的排查思路5.1 查询返回空值从标签拼写开始查新手最常遇到的问题是“我明明写了查询为什么结果是空的”。排查顺序我总结成三步第一去掉所有标签条件只留指标名看有没有数据第二逐个加回标签条件看加到哪个的时候变空第三检查那个标签的值是不是拼错了或者用了正则但没锚定导致匹配不上。还有一种情况是指标名本身带后缀。比如Counter类型在暴露时可能自动加了_total后缀你查http_requests查不到得查http_requests_total。这个后缀规则不同exporter可能不一样最稳妥的办法是在查询框里输入指标名前几个字符让自动补全告诉你完整名称。5.2 曲线出现断点采集间隔与窗口不匹配有时候曲线中间会断一截或者rate算出来忽高忽低。这通常是因为时间窗口和采集间隔没对齐。如果采集间隔是30秒你窗口设了1m那窗口内可能只有两个点rate的计算误差会很大如果某个时刻采集失败丢了一个点窗口内只剩一个点rate直接返回空。解决办法是把窗口放大到采集间隔的4到6倍。另外可以检查一下target的健康状态看是不是有采集超时或失败。如果是跨集群查询还要考虑时钟偏差的问题不同节点的时钟不同步会导致时间戳对不齐rate计算出现锯齿。5.3 聚合后数值翻倍重复序列没去重这个坑比较隐蔽。当你用sum()聚合时如果底层有重复的序列比如两个target采集了同一个指标结果会翻倍。常见于联邦部署或者relabel配置不当的场景。排查方法是先不加聚合直接看原始序列数一数同一个标签组合出现了几次。还有一种情况是by里漏了某个区分维度导致本该分开的序列被合并求和。比如两个机房的数据你按service分组但没带region两个机房的量就加在一起了。这种错误不会报错只会让数字看起来“偏高”很容易被忽略。6. 把查询写得更稳命名习惯与预计算6.1 给查询起个能看懂的名字PQL本身不支持给查询命名但你在面板或告警规则里是要写标题的。我的习惯是标题里带上“指标运算维度”比如“API P99延迟按路由”、“订单服务5xx速率按实例”。这样别人看面板不用点开查询详情就知道在算什么。另外查询本身也建议加注释。虽然PQL的注释语法#在部分界面里支持有限但在Recording Rule的配置文件里是可以写的。给每条规则写一行说明它算什么、为什么这么算三个月后你自己回来看会感谢当时的自己。6.2 Recording Rule把重查询变成轻查询如果一个查询被多个面板或告警反复使用而且它本身比较重涉及大窗口rate、多层聚合、子查询那就该用Recording Rule把它预计算成一个新指标。配置大概长这样groups: - name: api_rules rules: - record: api:request_rate5m expr: sum(rate(http_requests_total[5m])) by (service, handler)预计算之后面板直接查api:request_rate5m就行速度快一个数量级。命名上我习惯用level:metric:operation的格式冒号分隔一眼能看出这是聚合后的衍生指标。Recording Rule的评估间隔要和原始采集间隔协调好一般设成采集间隔的整数倍避免出现空洞。6.3 告警查询要留缓冲别卡在临界值上写告警用的PQL和写面板用的思路不太一样。面板可以实时反映瞬时值告警要考虑抖动。我的做法是告警条件里加一个for持续时间比如“错误率持续5分钟超过1%才触发”同时在查询里用稍长的窗口比如rate(...[5m])而不是[1m]让曲线平滑一些。阈值也不要卡得太死。如果SLO是99.9%可用性错误率阈值设1%看着合理但实际会因为正常波动频繁触发。留10%到20%的缓冲比如设1.2%配合for持续时间误报能少一大半。这些都是调出来的经验没有标准答案得根据自己系统的基线来定。写PQL这件事语法本身一两个小时就能过一遍真正花时间的是理解你的指标体系和业务语义。我建议初学者的练习路径是先对着现成的面板把每条查询拆开看懂然后试着自己改写——换个聚合维度、调个时间窗口、加个过滤条件看结果怎么变。改着改着你对“这条查询在算什么”的直觉就建立起来了。等到你能看着一个业务问题脑子里直接蹦出该用哪个指标、套哪个函数、按什么分组的时候PQL就算真正上手了。