Prometheus 监控实战 03:PromQL 查询语法与指标排查


前两篇已经把 Prometheus Server、Node Exporter 和 Pushgateway 都接起来了。现在 Prometheus 里至少有三类指标:

  1. Prometheus 自身指标。
  2. Linux 主机指标。
  3. Pushgateway 暂存的短任务指标。

这些指标如果只停留在“能查出来”,实际排查价值还不够。比如 node_cpu_seconds_total 是一个持续累加的 counter,直接看原始值并不能表示 CPU 使用率;node_memory_MemAvailable_bytes 是字节数,直接看也不直观;多个 target 的 up 指标散在一起时,也需要按 job 或 instance 聚合后才方便判断。

这篇就整理一批后面会反复用到的 PromQL 写法,先把过滤、聚合、rate、单位换算这些基础用熟。

1. 查询环境

当前 Prometheus 仍然采集三组 target:

scrape_configs:
  - job_name: "prometheus"
    static_configs:
      - targets: ["localhost:9090"]

  - job_name: "node_exporter"
    static_configs:
      - targets: ["localhost:9100"]

  - job_name: "pushgateway"
    honor_labels: true
    static_configs:
      - targets: ["localhost:9091"]

查询前先看一下服务器时间和 Prometheus 状态:

date '+%Y-%m-%d %H:%M:%S'
curl http://localhost:9090/-/ready

输出如下:

2025-04-27 16:03:11
Prometheus Server is Ready.

再用 API 查一下 up:

curl -s 'http://localhost:9090/api/v1/query?query=up'

Prometheus HTTP API 原始返回是 JSON,下面是整理后的关键结果,不是完整原始响应。可以看到三个 target 都是 1:

up{instance="localhost:9090", job="prometheus"} 1
up{instance="localhost:9091", job="pushgateway"} 1
up{instance="localhost:9100", job="node_exporter"} 1

页面查询结果如下:

up 是最常用的连通性指标。Prometheus 每次抓取 target 后,都会生成这个指标:

含义
1 本次 scrape 成功
0 本次 scrape 失败

所以做服务存活、Exporter 掉线、采集失败告警时,通常第一反应就是从 up 开始。但 up=1 只代表 Prometheus 最近一次 scrape 成功,不代表被监控服务业务完全正常。比如 Node Exporter 正常,只能说明主机指标接口可抓取,不能说明服务器没有 CPU、内存或磁盘压力。

在 Prometheus Web UI 里,Instant Query 适合看当前值,Graph 或 Range Query 适合看一段时间趋势。像 rate(x[5m]) 这类表达式,本质上会读取一段时间窗口里的样本,在图表里观察会更直观。复杂 PromQL 用 API 查询时建议使用 –data-urlencode,避免空格、括号、引号没有正确编码:

curl -G 'http://localhost:9090/api/v1/query' \
  --data-urlencode 'query=count by(job)(up)'

2. 通过 label 过滤指标

Prometheus 的每一条时间序列都由指标名和一组 label 共同决定。比如下面这条序列:

up{instance="localhost:9100", job="node_exporter"} 1

其中:

部分 说明
up 指标名
job=”node_exporter” 采集任务
instance=”localhost:9100” 具体采集端点
1 当前样本值

只看 Node Exporter 是否存活,可以加 label 过滤:

up{job="node_exporter"}

只看某一个实例:

up{instance="localhost:9100"}

如果要排除某类任务,可以用 !=:

up{job!="prometheus"}

PromQL 还支持正则匹配:

up{job=~"(node_exporter|pushgateway)"}

以及正则排除:

up{job!~"prometheus"}

这几个写法后面很常用,尤其是一个 Prometheus 同时采集很多服务时,先用 label 把范围收窄,查询结果才不会太乱。简单记一下:= 是精确匹配,!= 是精确排除,=~ 是正则匹配,!~ 是正则排除。PromQL 正则使用 RE2 语法,并且是完全匹配语义,所以复杂条件建议把分组写清楚。

3. 按 job 聚合 target 数量

直接查 up 会返回每个 target 一行。如果想快速看每个 job 当前有几个 target,可以用 count by(job):

count by(job)(up)

查询结果如下:

当前三个 job 每个都有一个 target:

{job="prometheus"}     1
{job="pushgateway"}    1
{job="node_exporter"}  1

这里统计的是每个 job 下的时间序列数量,也就是当前被采集的 target 数量。by(job) 表示按 job 分组。PromQL 聚合的关键就是理解“保留哪些 label”。比如:

count by(job)(up)

表示按 job 分组后计数,结果里只保留 job。

如果写成:

count(up)

则表示把所有 target 放在一起计数,结果只会得到一个总数:

{} 3

常见聚合函数如下:

函数 说明
count() 统计序列数量
sum() 求和
avg() 求平均值
max() 取最大值
min() 取最小值
topk(k, expr) 取前 k 个最大值
bottomk(k, expr) 取前 k 个最小值

聚合时可以用 by 保留指定 label,也可以用 without 去掉指定 label。

sum by(job)(up)
sum without(instance)(up)

这两个表达式在当前场景下结果接近,但语义不同。by(job) 是明确只保留 job,without(instance) 是去掉 instance,其他 label 仍可能保留。对 up 来说,sum by(job)(up) 更像是在统计成功 target 数量;如果某个 target 的值是 0,sum 结果会受影响。如果目的是统计 target 总数,更推荐 count by(job)(up)。实际写查询时,我更倾向于先用 by(…),结果更可控。

4. 用 rate 计算 CPU 使用率

Node Exporter 暴露的 CPU 指标是:

node_cpu_seconds_total

这个指标表示 CPU 在不同 mode 下累计消耗的秒数,属于 counter。直接查询会看到一个持续增长的累计值,不适合直接当 CPU 使用率。

常见做法是先算 idle 的增长速率,再用 100 - idle 得到使用率:

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

页面查询结果如下:

这个表达式可以拆开看:

rate(node_cpu_seconds_total{mode="idle"}[1m])

表示计算最近 1 分钟内 idle counter 每秒增长速度。单条 CPU 核 idle 序列通常在 0 到 1 之间,接近 1 说明这个核大部分时间都在空闲,接近 0 说明基本不空闲。多核机器需要按 instance 聚合后再理解整体使用率。

再按 instance 求平均:

avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[1m]))

如果机器有多核,这一步会把多个 CPU 核的 idle 比例平均到实例维度。

最后换算成百分比:

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

这里要注意两点:

  1. counter 类型指标通常不要直接看原始值,要结合 rate()、irate() 或 increase()。
  2. rate(x[1m]) 至少需要这个时间窗口里有多个样本,如果 Prometheus 刚启动,可能要等几个 scrape 周期才有结果。
  3. 如果 scrape_interval 是 15 秒,[1m] 只有几个采样点,适合实验时快速观察;生产告警通常用 [5m] 更稳。

rate()、irate() 和 increase() 的区别可以简单理解为:

函数 特点 常见用途
rate() 使用窗口内多个点计算平均速率,更平滑 告警、趋势图
irate() 使用最近两个点计算瞬时速率,更敏感 临时排查尖刺
increase() 计算窗口内累计增加量 统计一段时间内的请求数、处理数

做告警时一般优先用 rate(),临时看瞬间波动时再考虑 irate()。

5. 计算内存使用率

Node Exporter 暴露的内存指标比较直观:

node_memory_MemTotal_bytes
node_memory_MemAvailable_bytes

其中:

指标 说明
node_memory_MemTotal_bytes 总内存
node_memory_MemAvailable_bytes 系统估算的可用内存

如果只想看可用内存,可以先做单位换算:

node_memory_MemAvailable_bytes / 1024 / 1024 / 1024

如果要看内存使用率,可以写成:

(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100

页面查询结果如下:

这里使用的是 MemAvailable,不是简单的 MemFree。Linux 里很多内存会被 page cache、buffer 使用,但这部分在需要时可以释放,所以单看 MemFree 容易误判。用 MemAvailable 更接近“系统还能拿出来用的内存”。这两个指标的 label 基本一致,所以 PromQL 可以自动按相同 label 匹配计算;如果两个指标 label 不一致,就需要考虑 on()、ignoring()、group_left 这类匹配语法。

如果后面要做告警,可以先写一个比较宽松的表达式:

(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 80

这表示内存使用率超过 80%。不过这个表达式只是瞬时判断,真正告警规则里通常还要配合 for: 5m 或 for: 10m,避免短暂波动导致误报。

6. 查询 Pushgateway 指标

上一篇向 Pushgateway 推过一个短任务指标:

article_demo_processed_total

查询这个指标:

article_demo_processed_total

页面结果如下:

如果这里没有结果,需要先确认上一篇是否已经向 Pushgateway 推送过测试指标,并且这组指标没有被删除。可以看到结果里保留了推送时的 label:

article_demo_processed_total{instance="lavm-bzoq5mwl1h", job="article_demo"} 27

这和上一篇 Prometheus 配置里的 honor_labels: true 有关。Pushgateway 抓取到的指标本身已经带有 job、instance,保留这些 label 后,查询时看到的仍然是推送路径里的业务分组,而不是 scrape job pushgateway。

如果短任务有多个实例,可以按 job 求和:

sum by(job)(article_demo_processed_total)

如果要看某个任务最后一次成功时间,也可以查询上一篇推送的 gauge:

article_demo_last_success_unixtime

这个值是 Unix 时间戳。后面如果要做“任务长时间没有成功”的告警,可以用当前时间减去这个指标:

time() - article_demo_last_success_unixtime

比如超过 1 小时没有成功:

time() - article_demo_last_success_unixtime > 3600

这个写法比只看 article_demo_processed_total 更适合判断任务是否停滞。因为 counter 不变可能是任务没跑,也可能是任务跑了但没有新增处理量;最后成功时间能更直接地表达任务活性。

不过 Pushgateway 里的 counter 要谨慎理解。任务重跑、覆盖、删除后重新推送,都可能让 counter 语义不连续,它更适合配合明确的业务语义使用。另外,如果 article_demo_last_success_unixtime 指标不存在,time() - article_demo_last_success_unixtime > 3600 不会天然返回异常,而是没有结果。要覆盖“任务从未推送过指标”或“指标被删除”的情况,可以再加一个缺失检查:

absent(article_demo_last_success_unixtime)

7. 常用排查表达式

这里把当前环境里比较常用的表达式集中放一下。

7.1 target 是否存活

up

只看失败 target:

up == 0

按 job 统计失败数量:

count by(job)(up == 0)

这个表达式适合快速找失败对象,但只会返回存在失败 target 的 job。如果某个 job 没有失败 target,它不会返回该 job 的 0。

如果希望没有失败时返回 0,可以写成:

sum by(job)(up == bool 0)

这里要加 bool。如果直接写 up == 0,PromQL 会过滤出满足条件的序列,但保留原始样本值;失败 target 的原始值本来就是 0,再 sum 仍然是 0,不能表示失败数量。

sum by(job)(up == 0)

正确统计失败数量时,用下面这个写法:

sum by(job)(up == bool 0)

up == bool 0 会把比较结果转换成 1 或 0,这时再求和才是失败 target 数量。不过它也只能统计当前存在的 up 序列,不能替代 target 缺失检查。

7.2 CPU 使用率

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

如果想看更平滑一点的趋势,可以把窗口改成 5 分钟:

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

7.3 内存使用率

(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100

可用内存 GB:

node_memory_MemAvailable_bytes / 1024 / 1024 / 1024

7.4 磁盘使用率

Node Exporter 也会暴露文件系统指标。常见磁盘使用率可以这样写:

(1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|squashfs|proc|sysfs|devtmpfs|cgroup2?",readonly!="1"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay|squashfs|proc|sysfs|devtmpfs|cgroup2?",readonly!="1"}) * 100

如果只想看某个挂载点:

(1 - node_filesystem_avail_bytes{mountpoint="/",fstype!~"tmpfs|overlay|squashfs|proc|sysfs|devtmpfs|cgroup2?",readonly!="1"} / node_filesystem_size_bytes{mountpoint="/",fstype!~"tmpfs|overlay|squashfs|proc|sysfs|devtmpfs|cgroup2?",readonly!="1"}) * 100

磁盘指标里一定要注意过滤 tmpfs、overlay、proc、sysfs 这类虚拟或临时文件系统,否则容器、临时挂载点会把结果搞得很乱。磁盘使用率通常按 instance、mountpoint、device 区分,不建议直接对百分比做 sum。

7.5 HTTP 请求量

Prometheus 自身也有 HTTP 请求计数:

prometheus_http_requests_total

查看每秒请求量:

sum by(handler)(rate(prometheus_http_requests_total[1m]))

如果只看状态码:

sum by(code)(rate(prometheus_http_requests_total[1m]))

这类表达式后面接入 Flink、YARN 或其他服务时也很常见:先找到服务暴露的 counter,再用 rate() 转成单位时间内的变化量。

这里的 prometheus_http_requests_total 统计的是 Prometheus 自身 Web/API 请求量,不是业务服务的 HTTP QPS。迁移到业务服务或 Flink、YARN 这类组件时,前提是对应指标确实是 counter;gauge 类型指标不应该直接用 rate() 当作请求量。

7.6 查不到指标时怎么排查

PromQL 排查最怕一上来就盯着复杂表达式。我的习惯是按链路一层一层缩小范围:

up

先确认 target 是否还在、是否为 1。然后看 Prometheus 的 Targets 页面,确认 scrape URL 和错误信息。如果 target 是 UP,再直接访问 exporter 的 /metrics,确认指标源头是否真的暴露了这个指标。

不知道指标名时,可以先在 Prometheus UI 里输入前缀,比如 node_memory、node_filesystem。也可以用 API 看当前有哪些指标名:

curl -s 'http://localhost:9090/api/v1/label/__name__/values'

如果怀疑 label 写错,可以先查当前有哪些 job 和 instance:

curl -s 'http://localhost:9090/api/v1/label/job/values'
curl -s 'http://localhost:9090/api/v1/label/instance/values'

很多“查不到”的问题,最后不是指标不存在,而是 label 条件写得太窄,或者正则没有匹配上实际值。

8. 小结

这篇主要把 PromQL 的基础用法串了一遍:

  1. 用 label 过滤指标范围。
  2. 用 count by(…)、sum by(…) 做分组聚合。
  3. 用 rate() 把 counter 转成速率。
  4. 用四则运算做 CPU、内存、磁盘这类百分比指标。
  5. 用 time()、最后成功时间指标,以及必要时的 absent() 判断短任务是否长时间没有成功或指标缺失。

到这里,Prometheus 里已经不只是能看到原始指标,还可以把指标加工成更接近排查和告警的表达式。下一篇开始把 Flink 接进来,看 Flink 自身指标如何暴露给 Prometheus。后面接入 Flink 后,这些 PromQL 写法仍然会反复用到,比如 counter 用 rate(),按 job/task 聚合,以及按 label 过滤不同组件或任务。


文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录