前两篇已经把 Prometheus Server、Node Exporter 和 Pushgateway 都接起来了。现在 Prometheus 里至少有三类指标:
- Prometheus 自身指标。
- Linux 主机指标。
- 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
这里要注意两点:
- counter 类型指标通常不要直接看原始值,要结合 rate()、irate() 或 increase()。
- rate(x[1m]) 至少需要这个时间窗口里有多个样本,如果 Prometheus 刚启动,可能要等几个 scrape 周期才有结果。
- 如果 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 的基础用法串了一遍:
- 用 label 过滤指标范围。
- 用 count by(…)、sum by(…) 做分组聚合。
- 用 rate() 把 counter 转成速率。
- 用四则运算做 CPU、内存、磁盘这类百分比指标。
- 用 time()、最后成功时间指标,以及必要时的 absent() 判断短任务是否长时间没有成功或指标缺失。
到这里,Prometheus 里已经不只是能看到原始指标,还可以把指标加工成更接近排查和告警的表达式。下一篇开始把 Flink 接进来,看 Flink 自身指标如何暴露给 Prometheus。后面接入 Flink 后,这些 PromQL 写法仍然会反复用到,比如 counter 用 rate(),按 job/task 聚合,以及按 label 过滤不同组件或任务。