# 第 3 章 PromQL:把指标变成答案

本章导读

第 2 章结束时,你已经能在 Grafana 上看到四台主机的 CPU 曲线了。但那张图是别人写好模板给你的 —— 你并不知道里面那句表达式在算什么。

本章解决的就是这个问题。学完本章,你应当能够看着一条指标名,就知道该怎么查、能不能直接画、要不要套函数。

这是全课程技术含量最高的一章,也是最容易 "看懂但写不出" 的一章。建议边读边在 9090 的 Graph 页面里敲 ——PromQL 是肌肉记忆,不是阅读理解。

学习目标

序号学完本章,你应当能够
1说清 Sample、Time Series、Instant Vector、Range Vector、Scalar 的关系
2熟练使用四种匹配器与 offset ,写出精准的过滤条件
3判断一个指标属于 Counter / Gauge / Histogram / Summary,并决定该不该套函数
4用算术、比较、 bool 修饰符写出 "内存使用率"" 磁盘使用率 " 这类业务指标
5用 by / without 完成分组统计,正确计算 "占比" 类指标
6准确区分 increase / rate / irate / delta / idelta ,并说明各自适用场景
7理解 ignoring / on / group_left / group_right 解决什么问题
8排查 "表达式没结果" 和 "vector cannot contain metrics with the same labelset" 两类高频报错

# 3.1 先把地基夯死:时间序列与样本

# 3.1.1 时间序列(Time Series)

Prometheus 采用时间序列存储监控数据。

时间序列:按照时间顺序记录系统、设备或应用状态变化的数据集合。

数据按固定采集周期(如 15s、30s)不断采集,随着时间推移形成一串离散的数据点 —— 这就是时间序列。

放到坐标系里就非常直观:X 轴是时间,Y 轴是数值,把点连起来就是我们看到的监控曲线。Grafana 里那一条条折线,本质就是若干个时间序列。

# 3.1.2 样本(Sample)

每采集一次,就产生一个样本。一个 Sample 由两部分组成:

组成内容
keyMetric 名称 + Label 集合 + Timestamp(毫秒精度)
valuefloat64 格式的数值

📌 记住这个 "没有例外" 的事实:Prometheus 的样本值只能是 float64。
没有字符串、没有布尔、没有整型。你看到的 1 、 0 、 200 ,全是浮点数的字面表现。

# 3.1.3 唯一标识与五条铁律

时间序列的唯一标识 = Metric Name + Label Set。

围绕这句话,有五条推论必须记牢:

#铁律后果
1指标名 + 标签的特定组合代表一条时间序列查询时永远在 "选序列"
2不同指标名代表不同时间序列名字就是身份
3指标名相同、标签不同,是不同时间序列method="GET" 与 method="POST" 是两条
4更改任何标签值(含增删标签)都会创建新序列标签一改,历史数据就 "接不上"
5应尽量保持标签稳定标签乱变 → 序列爆炸 → 图形 / 告警 / 记录规则全部失效

⚠️ 第 4、5 条是生产事故级别的认知。第 4 章讲标签管理时,你会亲眼看到 "改了一个标签,图上出现断层" 的现象 —— 那正是这条铁律在起作用。

# 3.1.4 指标的两种书写形式

http_requests_total{method="GET", code="200"}          # 常规写法
{__name__="http_requests_total", method="GET"}         # 等价写法

第二种写法里出现了 __name__ —— 这就是第 1 章提过的内部保留标签,它存的正是指标名本身。

💡 为什么要知道 __name__ ? 因为它让你可以用正则批量选择指标名:

{__name__=~"prometheus_http_.*"}     # 所有以 prometheus_http_ 开头的指标

而在 metric_relabel_configs 里 "按指标名前缀批量丢弃" 时,用的也正是它。


# 3.2 四种表达式类型:PromQL 的 "类型系统"

# 3.2.1 什么是 PromQL

官方文档:https://prometheus.io/docs/prometheus/latest/querying/basics/

PromQL(Prometheus Query Language)是 Prometheus 内置的函数式表达式语言,用于实时查找和聚合时间序列数据。

它的结果有三个去处:

  1. 图表展示(Grafana / 内置 Graph);
  2. 表格展示(表达式浏览器);
  3. HTTP API 提供给外部系统(告警、自动化平台、自研大屏)。

📌 默认时间基准:不指定时间时,以 "当前时间" 为基准点取数。

# 3.2.2 四种结果类型

任何一个 PromQL 表达式(或子表达式),计算结果必为以下四种之一:

类型中文定义直观理解
instant vector即时向量一组时间序列,每条序列各含一个样本,共享同一时间戳一个时间点上的一组数
range vector范围向量一组时间序列,每条序列含一串随时间变化的样本一个时间段内的一组数
scalar标量一个简单的浮点数值一个 "裸数字",如 1024
string字符串一个简单字符串目前基本用不到
instant vector  →  node_load1
                   ↑ 4 台机器 = 4 条序列,每条 1 个值

range vector    →  node_load1[5m]
                   ↑ 4 台机器 = 4 条序列,每条 20 个值(15s×20=5m)

scalar          →  1024
string          →  "hello"

# 3.2.3 ⭐ 最实用的规则:什么场景要什么类型

这是新手最容易栽跟头的地方。三类约束必须背下来:

场景要求的类型违反后的报错
画折线图只能是 instant vector图表画不出来
用 rate / increase / irate 等速率函数必须是 range vector报 expected type range vector
在表达式浏览器里直接查 range vector❌ 不允许invalid expression type "range vector" for range query, must be Scalar or instant Vector

🧠 一句话总结:
范围向量永远不能 "出门",它只能在函数括号里活着。

所以要画图, node_load1[5m] 必须降维:

avg_over_time(node_load1[5m])     # range → instant,可以画了
rate(http_requests_total[5m])     # range → instant,可以画了

# 3.2.4 用 API 直接查数据

PromQL 不只能在界面里敲,它本质是一个 HTTP API。

# ① 即时查询:当前请求数
curl -G http://192.168.80.101:9090/api/v1/query \
  --data-urlencode 'query=prometheus_http_requests_total' | jq
# ② 指定时间点查询
curl -G http://192.168.80.101:9090/api/v1/query \
  --data-urlencode 'query=prometheus_http_requests_total' \
  --data-urlencode "time=$(date +%s)" | jq
# ③ 带标签过滤(正则)
curl -G http://192.168.80.101:9090/api/v1/query \
  --data-urlencode 'query=node_memory_MemFree_bytes{instance=~"192.168.80.(101|102):9100"}' | jq
# ④ 范围查询:必须给 start /end/step
curl -G http://192.168.80.101:9090/api/v1/query_range \
  --data-urlencode 'query=rate(node_cpu_seconds_total{mode!="idle"}[5m])' \
  --data-urlencode "start=$(date -d '-1 hour' +%s)" \
  --data-urlencode "end=$(date +%s)" \
  --data-urlencode 'step=15' | jq

💡 这套 API 的实际价值:所有 "把监控数据接到自研平台" 的需求,本质都是调 /api/v1/query_range 。学会它,你就能把 Prometheus 变成任何系统的数据源。

# 3.2.5 字面量:数字与字符串

# 数字:[-] digits [.digits]
2
2.43
-2.43
# 字符串:单引号 / 双引号 / 反引号
"this is a string"
'these are unescaped: \n \t'
`these are not unescaped: \n ' " \t`

反引号的作用:不做转义。写正则时特别有用(正则里的 \d 、 \d+ 用双引号写会被当作转义符处理)。


# 3.3 数据选择器:从百万序列里挑出你要的

# 3.3.1 统一语法

metrics_name{筛选label=值,...}[<时间范围>] offset <偏移>

一条表达式的三个可选部件:指标名 → 标签匹配器 → 时间修饰。

# 3.3.2 即时向量选择器(Instant Vector Selector)

获取 0 个、1 个或多个时间序列的即时样本值。

由两部分组成,至少要给出一个:

部分作用是否可省
指标名称限定是哪个指标可省
匹配器(Matcher)定义在 {} 内,过滤标签可省

# 用法一:只给指标名

node_filefd_allocated
prometheus_http_requests_total

💡 http_requests_total 与 http_requests_total{} 完全等价。空花括号不改变语义。

# 用法二:用 __name__ 匹配多个指标名

{__name__="prometheus_http_requests_total"}
{__name__=~"^prometheus_http_.*"}

# 用法三:只给匹配器

{job="node_exporter"}

⚠️ 这种写法会返回所有带该标签的指标 —— 可能横跨很多不同指标名,结果集极大。慎用。

# 用法四:指标名 + 匹配器(最常用)

http_requests_total{method="get"}

# ⭐ 四种匹配操作符

操作符含义说明
=精确匹配值完全相等
!=不匹配值不完全相等
=~正则匹配必须整值匹配(全锚定)
!~正则不匹配取反

关于 =~ 的全锚定,务必记住:

# ❌ 错误:以为能匹配 "get"、"gets"… 实际什么都匹配不到(除非值就是 "ge")
http_requests_total{method=~"^ge"}
# ✅ 正确:显式补齐
http_requests_total{method=~"^ge.*"}

🧠 Prometheus 的正则自动加了 ^...$ 。这是它和 grep 最大的区别,也是新手最高频的错误之一。

多值用 | 表示 "或":

{env=~"staging|testing|development"}

多条件用逗号 , 分隔(逻辑与):

prometheus_http_requests_total{instance="localhost:9090", job="prometheus"}
prometheus_http_requests_total{handler=~".*meta.*"}
node_memory_MemFree_bytes{instance=~"10.0.0.(101|102):9100"}
node_memory_MemAvailable_bytes{instance=~"10.0.0.20[0-9]:9100"}
node_memory_MemAvailable_bytes{instance!~"10.0.0.20[12]:9100"}

# ⚠️ 空标签值的特殊性

匹配到空值的匹配器,会让 "根本没有这个标签" 的时间序列也符合条件。

http_requests_total{env=""}

这条不仅匹配 env="" 的序列,还匹配完全不含 env 标签的所有 http_requests_total 序列。

# ⚠️ 一个硬性约束

向量选择器至少要包含一个指标名,或者至少一个 "非空值" 的标签匹配器。

所以下面这两种写法是非法的:

{job=~".*"}      # ❌ 正则能匹配空 → 相当于没限定
{job=""}         # ❌ 同上

💡 原因:这两种写法在语义上等于 "把整个数据库都选出来"。Prometheus 直接拒绝,防止你一条查询把内存打满。

# 3.3.3 范围向量选择器(Range Vector Selector)

在即时选择器后面加一个 [] 时间范围即可。

prometheus_http_requests_total{job="prometheus"}[5m]

时间单位的规则:

单位含义单位含义
s秒d日
m分w周
h时y年

两条限制:

[1h30m]      # ✅ 允许串联,且必须"由大到小"
[1.5h]       # ❌ 不允许小数,必须是整数时间单位

# ⭐ 为什么统计结果 "不那么整"?

这是一个非常好的理解 Prometheus 设计的机会。

不同时间序列的抓取时间点,并不严格对齐。

原因:如果所有 target 都在同一毫秒被抓取,几百上千个 target 会在那一瞬间把 Prometheus 压出尖峰。所以 Prometheus 会刻意为每个 target 稍微错峰(差几毫秒到几百毫秒)。

后果:趋势上准确,绝对数值上并不精确。 比如 [1m] 在 10s 间隔下,采样点数可能是 6 个,也可能是 5 个或 7 个。

💡 这解释了大量 " 为什么 increase() 算出来是 5.8 而不是 6 " 的困惑。不是 bug,是设计。

# 3.3.4 偏移修饰符 offset

所有选择器默认以 "当前时间" 为基准。 offset 就是把这根基准线往回推。

# 5 分钟之前的即时值
prometheus_http_requests_total offset 5m
prometheus_http_requests_total{code="200"} offset 5m
# 1 天之前的那 5 分钟区间
http_requests_total[5m] offset 1d
# 3 分钟之前的那 5 分钟区间
http_requests_total{handler="/metrics"}[5m] offset 3m

三条要点:

  1. offset 必须紧跟在选择器后面,中间不能断开;
  2. offset 偏移的是时间基点,不是区间长度;
  3. 既有范围又有偏移时:先定基点,再取范围。 [5m] offset 3m = "3 分钟前那一刻,往前数 5 分钟"。

💡 典型用途:环比

# 现在的 QPS 与 1 小时前对比
rate(http_requests_total[5m]) - rate(http_requests_total[5m] offset 1h)

# 3.4 四种指标类型:决定 "这个数能不能直接画"

# 3.4.1 先看一组真实的统计数据

# Prometheus 自身的指标类型分布
curl -s http://prometheus:9090/metrics | awk '$2=="TYPE"{t[$NF]++}END{for(i in t) print t[i], i}'
# 93 counter
# 74 gauge
#  7 histogram
# 12 summary
# Node Exporter 的指标类型分布
curl -s http://node01:9100/metrics | awk '$2=="TYPE"{t[$NF]++}END{for(i in t) print t[i], i}'
# 199 gauge
#  74 counter
#  47 untyped
#   1 summary

💡 这个小命令很值得收藏: awk '$2=="TYPE"' 直接统计某个 Exporter 暴露了多少种、什么类型的指标。评估一个 Exporter 的 "指标规模" 时非常好用。

注意 untyped 的存在 —— 它是历史遗留,行为接近 gauge,新写的 Exporter 不应该产出 untyped。

# 3.4.2 Counter(计数器)

单调递增的累计值,从 0 开始,只增不减,除非进程重启归零。

项目内容
特征只能增加,可被重置为 0(重新计数)
典型场景用户访问量、请求总数、任务完成数、错误总数
命名约定常以 _total 结尾
能否表示递减❌ 不能

范例:

# HELP node_cpu_seconds_total Seconds the CPUs spent in each mode.
# TYPE node_cpu_seconds_total counter
node_cpu_seconds_total{cpu="0",mode="idle"}    28375.49
node_cpu_seconds_total{cpu="0",mode="iowait"}      4.82
node_cpu_seconds_total{cpu="0",mode="irq"}         0
node_cpu_seconds_total{cpu="0",mode="nice"}        0
node_cpu_seconds_total{cpu="0",mode="softirq"}     0.09
node_cpu_seconds_total{cpu="0",mode="steal"}       0
node_cpu_seconds_total{cpu="0",mode="system"}      4.43
node_cpu_seconds_total{cpu="0",mode="user"}        0.36

# ⭐ 核心认知:Counter 的总数几乎没有意义

node_cpu_seconds_total{cpu="0",mode="idle"} = 28375.49 —— 这个数字本身说明不了任何问题。它只是 "从开机到现在,CPU 0 空闲了多少秒"。

有价值的是它的 "变化速度"。

所以 Counter 几乎从不直接使用,而是配函数降维:

函数得到什么用途
rate()每秒平均增长率⭐ 算 QPS、CPU 使用率
irate()最近两点的瞬时增长率看突发尖峰
increase()区间内增长总量算 "这段时间一共处理了多少"
topk()排名前 K 的序列找最忙的接口
# 找出请求总数排名前 3 的时间序列
topk(3, http_requests_total)
# 2 小时内的平均增长速率(各序列)
rate(http_requests_total[2h])
# 2 小时内最灵敏的瞬时速率
irate(http_requests_total[2h])

⚠️ Counter 重置的处理:进程重启后 Counter 归零,直接相减会得到负数。
rate() / irate() / increase() 已经内置处理了重置(自动检测下降并补偿),所以永远用函数算,不要自己写 current - offset 。

# 3.4.3 Gauge(计量器)

可增可减的瞬时状态值。

项目内容
特征任意波动,可以上升也可以下降
典型场景硬盘剩余空间、当前内存使用量、待处理任务数、并发请求数、负载
常见操作sum / avg / min / max / delta / predict_linear

范例:

# TYPE go_memstats_alloc_bytes gauge
go_memstats_alloc_bytes 2.017896e+06
# TYPE node_load1 gauge
node_load1  0
# TYPE node_load15 gauge
node_load15 0

两个实战函数:

# delta:区间首尾之差("比 2 小时前升高 / 降低了多少")
delta(cpu_temp_celsius{host="prometheus.example.com"}[2h])
# predict_linear:基于线性回归,预测 t 秒后的值
# ⭐ 容量预测:按最近 6 小时的趋势,预测 4 小时后根分区可用空间是否将小于 0
predict_linear(node_filesystem_free_bytes{mountpoint="/"}[6h], 4*3600) < 0

💡 最后这条表达式就是第 1 章讲的 " 趋势分析 " 的落地写法:不是等磁盘满了才告警,而是提前 4 小时预测它要满。

# 3.4.4 Histogram(直方图)

用于统计数据分布。服务端(Prometheus)负责计算分位数。

核心机制:累积桶(Cumulative Bucket)

把取值范围切成若干连续、相邻但不重叠的区间(bucket),统计每个区间里有多少个观测值。累积的意思是: le="0.1" 的桶里,包含了所有 ≤0.1 的样本,也自然被计入 le="0.5" 的桶。

一个 Histogram 指标会展开成多条时间序列:

序列含义
<basename>_bucket{le="<上界>"}落入该桶(含更小桶)的样本数
<basename>_bucket{le="+Inf"}全部样本数(最后一个桶)
<basename>_sum所有观测值的总和
<basename>_count观测总次数(本质是 Counter)

范例:

# TYPE prometheus_tsdb_compaction_chunk_range_seconds histogram
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="100"}        0
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="400"}        0
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="1600"}       0
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="6400"}       0
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="25600"}      0
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="102400"}     0
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="409600"}     0
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="1.6384e+06"} 0
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="6.5536e+06"} 6028
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="2.62144e+07"} 6028
prometheus_tsdb_compaction_chunk_range_seconds_bucket{le="+Inf"}      6028
prometheus_tsdb_compaction_chunk_range_seconds_sum   1.0734846288e+10
prometheus_tsdb_compaction_chunk_range_seconds_count 6028

读法:

  • 有 6028 次压缩,全部落在 6.55e+06 秒以内(因为从 le=6.5536e+06 开始就跳到 6028 了);
  • 平均耗时 = _sum / _count = 10734846288 / 6028 ≈ 178 万秒。

计算分位数:

# 过去 10 分钟,HTTP 请求持续时间的第 90 百分位数
histogram_quantile(0.9, rate(prometheus_http_request_duration_seconds_bucket[10m]))

Histogram 的三个优势:

  • 桶设计一次,服务端随意算(P50 / P90 / P99 都能事后算);
  • 支持聚合(多实例的桶可以相加后再算分位);
  • 单独提供 _sum 和 _count ,从而可以算平均值。

# 3.4.5 Summary(摘要)

与 Histogram 类似,但分位数在客户端就算好了。

一个 Summary 的序列构成:

序列含义
<basename>{quantile="<x>"}分位点 x 对应的值(0 ≤ x ≤ 1)
<basename>_sum样本值之和
<basename>_count样本总数

范例:

# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"}    3.4414e-05
go_gc_duration_seconds{quantile="0.25"} 5.0434e-05
go_gc_duration_seconds{quantile="0.5"}  6.5102e-05
go_gc_duration_seconds{quantile="0.75"} 9.2624e-05
go_gc_duration_seconds{quantile="1"}    0.00109096
go_gc_duration_seconds_sum   0.02311494
go_gc_duration_seconds_count 229

# 3.4.6 Histogram vs Summary:一表定生死

维度HistogramSummary
分位数在哪算Prometheus 服务端( histogram_quantile )客户端(每次采样时算好)
能否事后换分位数✅ 任意(P50/P90/P99 随时算)❌ 只能用客户端预定义的
能否多实例聚合✅ 桶可相加后再算❌ 分位数不可聚合
客户端开销小(只是计数)大(要维护滑动窗口与分位计算)
服务端开销大(分位计算消耗资源)小(直接读数)
准确性受桶划分影响,是估算值客户端流式计算,相对精确
典型用途⭐ 绝大多数场景首选(请求耗时、响应大小)分位数固定、实例少、不能聚合的场景

🧠 选型口诀:能用 Histogram 就用 Histogram。

因为 "分位数不能聚合" 是致命缺陷:你有 10 个实例,每个实例的 P99 是 100ms,整体 P99 绝不是 100ms—— 它可能差得很远。只有 Histogram 的桶相加后才能算整体的分位数。

唯一例外:客户端只需暴露一个固定的高分位数(如 SLO 的 P99.9),且不需要聚合。

⚠️ 严谨补充:笔记里说 Summary "不支持 sum 或 avg 一类的聚合运算"—— 这句话只对分位数成立。 <basename>_sum 和 <basename>_count 是可以正常聚合的(它们本质是 Counter)。所以 Summary 可以算平均值,不能算分位数。

# 3.4.7 一张决策树

flowchart TD
    A["拿到一个指标"] --> B{"名字带 _total<br/>或 HELP 写 counter?"}
    B -->|是| C["Counter<br/>必须配 rate / irate / increase"]
    B -->|否| D{"名字带 _bucket<br/>或 quantile?"}
    D -->|_bucket| E["Histogram<br/>histogram_quantile 算分位"]
    D -->|quantile| F["Summary<br/>直接读分位值"]
    D -->|都不是| G["Gauge<br/>可直接画 / sum / avg / delta"]

# 3.5 二元运算符:从 "原始数据" 到 "业务指标"

官方文档:https://prometheus.io/docs/prometheus/latest/querying/operators/

三类二元运算符:算术、比较、逻辑。

# 3.5.1 算术运算符

运算符含义
+加
-减
*乘
/除
%取模
^幂运算

最实用的四个用法:

# ① 单位换算:字节 → MB(除两次 1024)
node_memory_MemFree_bytes / 1024 / 1024
node_memory_MemFree_bytes / (1024 * 1024)      # 等价写法
# ② 可用内存百分比
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100
# ③ 内存使用率
(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes)
  / node_memory_MemTotal_bytes * 100
# ④ 根分区磁盘使用率
(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_free_bytes{mountpoint="/"})
  / node_filesystem_size_bytes{mountpoint="/"} * 100

🔗 呼应第 1 章:1.4 节讲 "数据处理" 时,我们推导过内存使用率公式;1.5.1 节讲 free vs available 时,我们强调过 "只看 available"。上面第 ③ 条就是那个知识点的最终落地形态。

# ⭐ 关于 "字节换算成 GB"

严格来说 1 GB = 1024³ 字节 = 1073741824。所以:

node_memory_MemTotal_bytes / 1024 / 1024 / 1024     # 严格的 GiB

但很多运维习惯用 1024^3 而口语称 "GB"。在面板上标注清楚单位(GiB 还是 GB),比争论哪个更对更有意义。

# 3.5.2 比较运算符

运算符含义
==等于
!=不等于
> >=大于 / 大于等于
< <=小于 / 小于等于

# ⭐ 关键区别:加不加 bool

这是 PromQL 与 "普通编程语言" 最大的不同点,必须理解透。

写法语义结果
expr > 80过滤(filter)只保留 "比较成立" 的序列,不成立的直接消失
expr > bool 80转换(bool)保留所有序列,成立的输出 1 ,不成立的输出 0
# 只看 "内存利用率 > 80%" 的主机(结果集里没有那些主机,它们 "消失" 了)
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 80
# 输出 0/1,所有主机都保留(适合画状态图、做告警计数)
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > bool 80
# 请求数是否超过 1000 → 超了是 1,没超是 0
prometheus_http_requests_total > bool 1000

📌 官方说明:使用 bool 后,布尔运算不会过滤时间序列,而是依次把每个样本与标量比较,结果 0 或 1 形成一条新时间序列。

🎯 告警规则里大量使用 +bool 会比较 "好用",但要注意:告警规则本身用 expr > 80 就够了(Alertmanager 只需要 "有没有结果")。 bool 更适合画 "状态图" 和做 "计数"(如 count(up == bool 0) 数挂了几台)。

# 3.5.3 逻辑运算符(集合运算)

运算符含义类比 SQL
and交集INNER JOIN / INTERSECT
or并集UNION
unless补集(排除)EXCEPT / NOT IN
# 并集:两个序列都出现(注意两者指标名不同,结果会各自保留)
node_memory_MemTotal_bytes{instance="10.0.0.104:9100"}
  or node_memory_MemFree_bytes{instance="10.0.0.105:9100"}
# 交集:两边都存在的序列才保留(左侧的序列留下,但只用左侧的 "身份" 匹配)
node_memory_MemTotal_bytes{instance="10.0.0.101:9100"}
  and node_memory_MemFree_bytes{instance="10.0.0.104:9100"}
# 补集:从左侧排除掉右侧匹配到的
node_memory_MemTotal_bytes{job="k8s-node"}
  unless node_memory_MemTotal_bytes{instance="10.0.0.104:9100"}

⚠️ 注意 and 容易踩的坑:
and 的结果保留左侧的序列和值,右侧只是用来 "做匹配判断"。
上面第二个例子里,左右两边的 instance 不同(101 vs 104),没有共同的标签集 → 交集为空 → 结果什么都没有。这是新手最迷惑的地方。

📌 限制: and / or / unless 只支持两个即时向量之间运算,不支持标量参与。
(算术与比较运算符是可以和标量运算的,例如 * 100 、 > 80 。笔记里 "不支持标量参与运算" 这句话,仅针对逻辑运算符。)

# 3.5.4 运算符优先级

从高到低:

优先级运算符
1^
2* / %
3+ -
4== != <= < >= >
5and unless
6or

规则:

  • 同优先级左结合( ^ 例外,它是右结合);
  • 用 () 改变次序 —— 永远推荐显式加括号,可读性远胜于背优先级表。
# 建议这样写,而不是依赖优先级
(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes * 100

# 3.6 向量匹配:让两个指标 "对上号"

# 3.6.1 默认规则:标签必须完全一致

两个向量做二元运算时,Prometheus 要给左边每条序列在右边找到 "同一条" 序列。

默认要求:除 __name__ 之外,所有标签必须完全一致。

大部分时候这没问题(比如 sum / count ,都是同一条序列的总和与总数)。但一旦两边标签 "不完全一样",就匹配不上。

go_gc_duration_seconds_sum{instance="prometheus:9090", job="prometheus"}
  / go_gc_duration_seconds_count{instance="prometheus:9090", job="prometheus"}

上面这样能算(两边标签一致),但如果写成:

go_gc_duration_seconds_sum / go_gc_duration_seconds_count      # ✅ 也能算(同名标签)

一旦两侧标签集不同,就需要匹配修饰符。

# 3.6.2  ignoring 与 on

关键字含义
ignoring(<labels>)匹配时忽略这些标签
on(<labels>)匹配时只使用这些标签
<vector expr> <bin-op> ignoring(<label list>) <vector expr>
<vector expr> <bin-op> on(<label list>) <vector expr>

# 3.6.3 一对一匹配(One-to-one)

用官方例子把这件事讲透。 输入数据:

# 左侧:按 method + code 统计的 5 分钟错误率
method_code:http_errors:rate5m{method="get",  code="500"} 24
method_code:http_errors:rate5m{method="get",  code="404"} 30
method_code:http_errors:rate5m{method="put",  code="501"}  3
method_code:http_errors:rate5m{method="post", code="500"}  6
method_code:http_errors:rate5m{method="post", code="404"} 21
# 右侧:按 method 统计的 5 分钟总请求量
method:http_requests:rate5m{method="get"}  600
method:http_requests:rate5m{method="del"}   34
method:http_requests:rate5m{method="post"} 120

需求:算出 "每个 method 下 code=500 的请求占比"。

method_code:http_errors:rate5m{code="500"} / ignoring(code) method:http_requests:rate5m

为什么必须加 ignoring(code) ?

因为左边有 method 和 code 两个标签,右边只有 method 。不忽略 code ,两边标签永远对不上,结果为空。

结果:

{method="get"}  0.04    # 24 / 600
{method="post"} 0.05    #  6 / 120

注意两个现象:

  • 结果里没有 put 和 del —— 因为右侧没有 put 的序列, del 左侧也没有对应项,无匹配的条目直接被丢弃;
  • 结果是一条序列(每个 method 一条),因为 code 被忽略掉了。

# 3.6.4 多对一 / 一对多: group_left 与 group_right

上例中,如果我想同时算出 500 和 404 的占比,左侧一个 method 对应多条序列(多个 code),右侧一个 method 只有一条 —— 这就是多对一。

method_code:http_errors:rate5m / ignoring(code) group_left method:http_requests:rate5m

结果:

{method="get",  code="500"} 0.04    # 24 / 600
{method="get",  code="404"} 0.05    # 30 / 600
{method="post", code="500"} 0.05    #  6 / 120
{method="post", code="404"} 0.175   # 21 / 120

group_left / group_right 的语义:

关键字含义记忆法
group_left左侧为 "多",右侧为" 一 "把右侧那一条值,广播给左侧的多条
group_right右侧为 "多",左侧为" 一 "反方向

🧠 一句话记住: left / right 标记的是 "多" 的那一侧。
名字有点反直觉 —— 它不是说 "多的一方在左边",而是说 " 多的一侧是 left(或 right)"。

完整语法(四个字段可组合):

<vector expr> <bin-op> ignoring(<labels>) group_left(<labels>)  <vector expr>
<vector expr> <bin-op> ignoring(<labels>) group_right(<labels>) <vector expr>
<vector expr> <bin-op> on(<labels>)      group_left(<labels>)  <vector expr>
<vector expr> <bin-op> on(<labels>)      group_right(<labels>) <vector expr>

💡 group_left(...) 括号里可以写 " 要从右侧额外带过来的标签 "。这是 kube-prometheus 里做 kube_pod_info * on(pod) group_left(node) ... 这类关联时的关键技巧。

但请注意课件的态度:向量匹配本身比较复杂,初级运维阶段用得不多。先理解 "为什么需要 ignoring",等 4.x 讲 k8s 与服务发现时再回头啃多对多。

# 3.6.5 ⭐ 高频报错: vector cannot contain metrics with the same labelset

先看一个 "看起来对、其实错" 的写法:

# ❌ 报告里常见的错误写法
prometheus_http_requests_total{code="200", handler="/-/reload", instance="localhost:9090"}
  / sum(prometheus_http_requests_total{handler="/-/ready", instance="localhost:9090"})

问题在哪?

  • 左侧:带 code 、 handler 、 instance 、 job 四个标签的一条序列;
  • 右侧: sum() 之后所有标签被清空(只剩空标签集)的一条序列。

两边标签完全对不上 → 无匹配 → 结果为空。(不是语法错,是逻辑错,最坑的一类)

正确写法(两边都聚合成同样 "无标签" 的形态):

# ✅ 200 响应码占全部请求的百分比
sum(prometheus_http_requests_total{code="200", handler="/-/reload", instance="localhost:9090"})
  / sum(prometheus_http_requests_total{instance="localhost:9090"})

另一个典型场景:

# ✅ /metrics 这个 handler 占所有 handler 的百分比
sum(prometheus_http_requests_total{handler="/metrics", instance="localhost:9090"})
  / sum(prometheus_http_requests_total{instance="localhost:9090"})

💡 规律:算 "占比" 时,分子分母必须用相同的聚合维度。
分子 sum(...) 单序列、分母 sum(...) 单序列 → 一对一,完美匹配。


# 3.7 聚合操作:从 "一堆数" 到 "一个结论"

# 3.7.1 为什么必须聚合

单个指标的价值不大。 监控场景中往往需要把一组指标 "联合" 起来看。

聚合的本质:对一组值进行计算,返回单个值或少量值 —— 也就是数据库里说的 "聚合函数"。

# 3.7.2 11 种聚合操作

操作符作用
sum()求和
avg()求平均值(指标数据分析的标准方法)
min()最小值
max()最大值
count()统计时间序列条数
count_values()按样本值分组计数(值计数)
topk()降序返回前 K 个最大值的序列
bottomk()升序返回前 K 个最小值的序列
quantile()返回分组内指定分位数
stddev()标准差(波动程度)
stdvar()方差(求标准差的中间量)

两种格式:

# 普通聚合
聚合操作符(表达式)                     →  sum(node_memory_MemTotal_bytes)
# 需要额外参数的聚合
聚合操作符(参数, 表达式)               →  topk(5, http_requests_total)
                                         count_values("os_version", node_os_version)
                                         quantile(0.9, http_request_duration_seconds)

# 3.7.3 ⭐ by 与 without :等价于 SQL 的 GROUP BY

这是本章最重要的语法点之一。

关键字语义类比
by(<labels>)只按这些标签分组统计,结果里只保留这些标签GROUP BY <labels>
without(<labels>)排除这些标签,用其余所有标签分组统计GROUP BY 除这些之外的所有列

两种书写位置,完全等价:

# 格式 1:by/without 写在括号外(推荐,读起来更像 SQL)
sum(node_memory_MemTotal_bytes) by (instance)
sum(node_memory_MemTotal_bytes) without (instance, job)
# 格式 2:by/without 写在函数名后
sum by (instance) (node_memory_MemTotal_bytes)
sum without (instance, job) (node_memory_MemTotal_bytes)

💡 选哪个? 官方文档示例用格式 2 较多,但格式 1 更接近 SQL 直觉。团队统一即可,别混用。

# 3.7.4 一串由浅入深的例子

先看数据长什么样:

node_os_version{id="ubuntu", id_like="debian", instance="10.0.0.200:9100", job="node_exporter", name="Ubuntu"} 1
node_os_version{id="ubuntu", id_like="debian", instance="10.0.0.201:9100", job="node_exporter", name="Ubuntu"} 1
node_os_version{id="ubuntu", id_like="debian", instance="10.0.0.202:9100", job="node_exporter", name="Ubuntu"} 1
node_os_version{id="ubuntu", id_like="debian", instance="10.0.0.203:9100", job="node_exporter", name="Ubuntu"} 1

① 数一数有几台 Ubuntu 主机

count(node_os_version{id="ubuntu"})        # → 4

② 按操作系统版本分别统计数量

count_values("os_version", node_os_version)
# {os_version="22.04"} 4
# {os_version="20.04"} 2

💡 count_values("标签名", expr) 会创建一个新标签,值为样本值本身,然后统计每个值出现了几次。这是 "按值分布统计" 的神器。

③ 所有主机的内存总和

sum(node_memory_MemTotal_bytes)            # 单条结果,无标签

④ 每台主机的内存总量(分组)

sum(node_memory_MemTotal_bytes) by (instance)
# {instance="10.0.0.101:9100"} 2.1e+09
# {instance="10.0.0.102:9100"} 2.1e+09
# ...

⚠️ 注意一个容易迷惑的现象: sum(...) without (instance, job) 和 sum(...) by (instance) 结果不同!

  • by(instance) → 结果只有 instance 标签;
  • without(instance, job) → 结果保留 instance / job 之外的所有标签(比如 cpu 、 mode )。

想清楚 "我要的结果带哪些标签",再决定用 by 还是 without 。

⑤ 数每台主机的 CPU 核心数

count(node_cpu_seconds_total{mode="system"})              # 所有主机加起来多少个核
count(node_cpu_seconds_total{mode="system"}) by (instance) # 每台主机几个核

💡 这个技巧很实用: node_cpu_seconds_total 每条序列对应一个 cpu 核心,数序列条数 = 数核心数。

⑥ 找最忙的接口 / 最大的值

max(prometheus_http_requests_total)
max(prometheus_http_requests_total) by (handler, instance)
topk(5, prometheus_http_requests_total)      # 前 5 大
bottomk(5, prometheus_http_requests_total)   # 前 5 小

💡 topk / bottomk 相当于 SQL 的 ORDER BY ... LIMIT K 。注意: topk 会保留原始标签(不是聚合掉),所以你能看到 "是哪条序列"。

⑦ 按 mode 分组看 CPU 时间

sum(node_cpu_seconds_total) by (mode)

⑧ 多维度分组

count(prometheus_http_requests_total) by (code, instance, job)

# 3.7.5 ⭐ 报错现场:算 "占比" 时的标签不匹配

这是运维工作中最高频的 PromQL 报错,值得单独讲。

# ❌ 报错:vector cannot contain metrics with the same labelset
nginx_http_requests_total{path="/api", method="GET", code="200"}
  / sum(nginx_http_requests_total{path="/api", method="GET"})

症状:要么报 labelset 冲突,要么返回空。

根因:

侧标签集
左path 、 method 、 code 、 instance 、 job
右空( sum() 清空了所有标签)

标签对不上 → 要么无匹配(空结果),要么在全无标签的情况下多对一冲突。

正确写法 —— 两边聚合到同一粒度:

# ✅ 200 占比
sum(nginx_http_requests_total{path="/api", method="GET", code="200"})
  / sum(nginx_http_requests_total{path="/api", method="GET"})
  * 100

如果确实要保留 code 维度:

# ✅ 每个 code 占该 path+method 总量的比例
sum(nginx_http_requests_total{path="/api", method="GET"}) by (code)
  / ignoring(code) group_right
  sum(nginx_http_requests_total{path="/api", method="GET"})

🎯 面试加分点:能说出 "算占比时分子分母必须同粒度聚合" 并且知道用 ignoring + group_right 保留维度,说明你真的写过 PromQL,不是背的。


# 3.8 功能函数:让 Counter 变得有意义

官方文档:https://prometheus.io/docs/prometheus/latest/querying/functions/

Prometheus 内置函数有数十个,按官方分组主要分四类:

分类代表函数
计算相关abs deriv exp ln log2 log10 sqrt ceil floor round delta idelta increase rate irate resets holt_winters histogram_quantile predict_linear *_over_time
取样相关absent sort sort_desc changes scalar clamp_max clamp_min absent_over_time
时间相关time timestamp hour minute month year day_of_month day_of_week days_in_month
标签相关label_join label_replace

# 3.8.1 取整三兄弟(别搞混)

ceil(node_load15 * 10)      # 向上取整:4.1 → 5, 4.9 → 5
floor(node_load15 * 10)     # 向下取整:4.1 → 4, 4.9 → 4  ← 直接砍掉小数,不看大小
round(node_load15 * 10)     # 四舍五入:4.1 → 4, 4.9 → 5

🧠 floor 不是四舍五入! 它是 "把小数部分直接删掉"。 floor(4.9) = 4 ,这跟 round(4.9) = 5 完全不同。这是最容易答错的一个。

# 3.8.2 ⭐ 核心四兄弟: increase / rate / irate / delta

这是全章最需要 "真的理解" 的部分。

先看它们各自的定义:

函数输入输出一句话
increase(v[d])range vector区间内增长总量"这段时间一共涨了多少"
rate(v[d])range vector每秒平均增长率"平均下来每秒涨多少"
irate(v[d])range vector最近两点的瞬时增长率"现在这一刻每秒涨多少"
delta(v[d])range vector区间首尾差值(可负)给 Gauge 用,不是给 Counter 用
idelta(v[d])range vector最近两点差值给 Gauge 用

⚠️ 先纠正一个常见误解: increase() 不是 "最后两个点相减"。
它等于 rate(v[d]) × 区间秒数 ,也就是基于区间首尾值做外推估算得到的增长总量。
(笔记里写 "increase = last 值 − last 前一个值",那是 idelta 的语义,不是 increase 。)

# 用 "薪水" 理解 increase 和 rate (讲师原话,非常好用)

假设你的月薪从 1 万 涨到了 2 万:

  • increase = 1 万(一共涨了 1 万)
  • 但这个数字意义有限 —— 因为你还得知道 "用了多久":
    • 一年涨 1 万 → 变化率 = 1 万 / 年
    • 十年涨 1 万 → 变化率 = 1000 / 年

结论:光看 "涨了多少" 意义不大,"单位时间涨了多少" 才有意义。这就是 rate 。

rate=区间末值−区间首值区间秒数\text{rate} = \frac{\text{区间末值} - \text{区间首值}}{\text{区间秒数}}

# rate 的计算方式与 "被平均" 问题

假设采集间隔 10s,取 [1m] ,区间内有 6 个采样点:

t=0s    t=10s   t=20s   t=30s   t=40s   t=50s   t=60s
 100     110     120     130     140     150     160
  ↑                                              ↑
首值                                           末值

rate   = (160 - 100) / 60 = 1.0   ← 只用首尾两个点,中间 4 个点被"忽略"

问题:如果中间发生了剧烈波动(比如 t=50s 时突然涨到 500 又回落), rate 完全看不出来 —— 因为它已经把这段变化 "平均" 掉了。

# irate 的计算方式:只看最后两步

irate = (末值 - 末值前一个点) / 这两点的时间差
      = (160 - 150) / 10 = 1.0    ← 只看最后两个点

优势:能反映最近一瞬间的变化,尖峰不会被抹平。

# 一张图对比

场景:前 50 秒请求量平稳(0~10),最后 10 秒暴增到 100+

         rate(平均)              irate(瞬时)
         ┌────────────────┐        ┌────────────────┐
   数值  │      ╱         │        │              ╱ │
        │    ╱           │        │            ╱   │
        │  ╱             │        │          ╱     │
        └────────────────┘        └────────────────┘
          平缓、削平了峰值            尖锐、捕捉到尖峰

   适合:SLO 统计、容量规划        适合:突发流量、异常检测

📌 官方建议:

  • irate 适合快速变化的计数器(如高 QPS 的接口);
  • rate 适合缓慢变化的计数器(如磁盘 IO、网卡流量)。
  • 对快速变化的计数器用 rate ,会削平峰值;除非把区间设得很小来减弱这种效应。

# 实战写法

# ① 单块磁盘过去 1 分钟的读取速率
rate(node_disk_read_bytes_total[1m])
irate(node_disk_read_bytes_total[1m])
# ② 网卡出口流量(MB/s)
rate(node_network_transmit_bytes_total{device="eth0"}[1m]) / 1024 / 1024
# ③ 最近 1 分钟 CPU 空闲时间增量
increase(node_cpu_seconds_total{cpu="0", mode="idle"}[1m])
# ④ ⭐ CPU 使用率(最经典的写法)
(1 - avg by (instance) (irate(node_cpu_seconds_total{mode="idle"}[5m]))) * 100

第 ④ 条拆解:

  1. node_cpu_seconds_total{mode="idle"}[5m] —— 所有 CPU 所有核的空闲时间(range vector)
  2. irate(...) —— 每个核的空闲速率(instant vector)
  3. avg by (instance)(...) —— 把 "多核" 平均掉,得到 "这台机器的平均空闲率"(0~1)
  4. 1 - ... —— 得到使用率(0~1)
  5. * 100 —— 转成百分比

💡 为什么用 avg by (instance) 而不是 sum ?
因为每个核的 idle 速率已经是 0~1 的比例,平均才对; sum 会得到 "核心数" 倍的值,完全错。
这是 PromQL 里最容易算错的指标,面试高频。

# 另一个经典:QPS

# 每秒请求数(QPS)
sum by (handler) (rate(prometheus_http_requests_total[5m]))
# 5xx 错误率是否超过总请求的 10%
rate(http_requests_total{status_code=~"5.*"}[5m])
  > rate(http_requests_total[5m]) * 0.1

# 3.8.3  time() 与运行时长

# 每台主机已运行多少小时
(time() - node_boot_time_seconds) / 3600
# 所有主机运行时间总和
sum(time() - node_boot_time_seconds) / 3600

💡 time() 返回 "当前时间戳", node_boot_time_seconds 返回 "开机时间戳",相减就是 uptime—— 这是一个非常经典的 "两个指标相减" 的用法。

# 3.8.4  histogram_quantile :算分位数

# 过去 10 分钟 HTTP 请求耗时的 P90
histogram_quantile(0.9, rate(prometheus_http_request_duration_seconds_bucket[10m]))

三个使用要点:

  1. 必须配合 rate() ( _bucket 是 Counter,累加值不能直接算分位);
  2. 必须用 _bucket 序列,不是 _sum / _count ;
  3. 结果只是估算值,精度取决于桶的划分。桶太少、太粗,误差会很大。

按维度分组算:

histogram_quantile(0.9,
  sum by (le, handler) (rate(http_request_duration_seconds_bucket[5m]))
)

⚠️ 注意 sum by (le, handler) 里必须保留 le ! 丢了 le ,函数就不知道桶边界,直接报错。这是新手最常见的 histogram_quantile 错误。

# 3.8.5  absent :专门为告警设计

absent(node_memory_SwapTotal_bytes)
情况返回值
有数据空(无结果)
没数据1

💡 用途:"指标消失了" 本身就是要告警的事件。 比如某个 Exporter 挂了, node_memory_* 全没了 —— 这时候正常表达式会返回空,告警不会触发,你就 "什么都不知道"。用 absent() 才抓得住这种 "彻底失联"。

这条是告警规则设计的必备技巧,第 5 章会反复用到。

# 3.8.6 标签处理函数

# label_replace:用正则重命名 / 改写标签
label_replace(up, "host", "$1", "instance", "(.*):.*")
# label_join:把多个标签拼成一个
label_join(up, "endpoint", "/", "instance", "job")

⚠️ 重要提醒: label_replace / label_join 是查询时的临时改写,不会改变存储中的序列。真正的 "标签管理" 要用第 4 章的 relabel_configs / metric_relabel_configs —— 那是落盘的、永久的。


# 3.9 本章小结

mindmap
  root(("第3章<br/>PromQL"))
    类型系统
      instant vector 可画图
      range vector 只能进函数
      scalar
      string
    选择器
      指标名
      四种匹配器
      正则全锚定
      范围 m h d w y
      offset 移基点
    指标类型
      Counter 配 rate
      Gauge 可直画
      Histogram 服务端算分位
      Summary 客户端算分位
    二元运算
      算术 单位换算
      比较 bool 不过滤
      逻辑 and or unless
      向量匹配 ignoring group
    聚合
      sum avg min max count
      topk bottomk
      count_values
      by without
    函数
      rate 平均
      irate 瞬时
      increase 增量
      delta Gauge 差值
      histogram_quantile
      predict_linear
      absent

七条必须带走的知识:

#知识为什么重要
1范围向量不能直接画图必须先降维( rate / avg_over_time )
2=~ 是整值匹配不写 .* 就匹配不到
3Counter 的总数没意义必须配 rate / irate / increase
4> 80 过滤、 > bool 80 转换决定结果里 "有没有这条序列"
5rate 被平均、 irate 抓瞬时选错就看不到尖峰
6算占比分子分母必须同粒度否则空结果或 labelset 报错
7CPU 使用率用 avg by(instance) ,不是 sumsum 结果会乘上核心数

# 3.10 面试高频问答

Q1: rate 和 irate 有什么区别?分别什么时候用?

rate 用区间首尾两个值相减再除以区间秒数,得到平均增长率,会把中间的波动抹平; irate 用最后两个采样点相减除以它们的时间差,得到瞬时增长率,能反映尖峰。
快速变化的计数器(高 QPS 接口、突发流量)用 irate ;缓慢变化的计数器(磁盘 IO、网络流量、SLO 统计)用 rate 。

Q2:为什么不能直接画 node_cpu_seconds_total ?

因为它是 Counter,值是 "开机至今的累计秒数",单调递增,画出来就是一条斜线,毫无意义。必须用 rate / irate 转成 "每秒的增长率" 才有观察价值。

Q3: by 和 without 有什么区别?

by(labels) 是白名单:只按这些标签分组,结果只保留这些标签,等价于 SQL 的 GROUP BY 。
without(labels) 是黑名单:排除这些标签,用其余全部标签分组,结果保留除它们外的所有标签。

Q4:Histogram 和 Summary 怎么选?

优先 Histogram。因为 Histogram 的分位数由服务端用 histogram_quantile 计算,桶可以跨实例相加,所以能算全局分位数,也能事后换分位数;Summary 的分位数在客户端算好,不可聚合(10 个实例的 P99 不能相加得到整体 P99),只能用预定义的分位点。
Summary 只适合分位数固定、实例少、不需要聚合的场景。代价是 Histogram 受桶划分影响,结果是估算值。

Q5: absent() 有什么用?

检测 "指标完全消失" 的情况。有数据返回空,无数据返回 1。因为普通表达式在指标消失时返回空结果,告警不会触发,而 "Exporter 挂了" 恰恰是最需要告警的场景。

Q6: > 80 和 > bool 80 有什么区别?

> 80 是过滤:只保留满足条件的序列,不满足的直接从结果中消失。
> bool 80 是转换:保留所有序列,满足的返回 1,不满足的返回 0,从而形成一条新的 0/1 时间序列。适合画状态图和做计数。

Q7:算占比时经常没结果,怎么排查?

九成是分子分母聚合粒度不一致:一边带多个标签、一边被 sum() 清空,导致两边标签集对不上、无法匹配。解决方法是两边都用相同维度聚合(如都 sum() ),或者用 ignoring(...) / group_left 明确匹配规则。


# 3.11 动手实验

# 实验一 类型识别训练

打开 http://<node>:9100/metrics ,找出下面这些指标,判断类型并写出正确的查询写法:

指标类型正确写法
node_cpu_seconds_total??
node_memory_MemAvailable_bytes??
node_filesystem_size_bytes??
node_load1??
node_boot_time_seconds??

参考答案要点: cpu_seconds_total 是 Counter → 配 rate ; MemAvailable 是 Gauge → 直接算比例; filesystem_size 是 Gauge → 算使用率; load1 是 Gauge → 直接画或 avg_over_time ; boot_time 是 Gauge(但语义是时间戳)→ 配 time() 相减。

# 实验二 写出五个业务指标

在 Graph 页面依次跑通,并解释每一步:

# 1. 单位换算:可用内存(MB)
node_memory_MemAvailable_bytes / 1024 / 1024
# 2. 可用内存百分比
node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100
# 3. 内存使用率 > 70% 的主机(过滤)
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 70
# 4. 同上,但输出 0/1(转换)
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > bool 70
# 5. 根分区使用率
(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_free_bytes{mountpoint="/"})
  / node_filesystem_size_bytes{mountpoint="/"} * 100

思考题:第 3 条和第 4 条的结果有什么不同?分别在什么场景下用?

# 实验三  rate vs irate 的肉眼对比

# 制造流量尖峰:平稳访问 → 突然猛刷
while true; do curl -s http://<your-app>:8001/metrics > /dev/null; sleep 1; done
# 另一个终端执行 "暴刷"
while true; do curl -s http://<your-app>:8001/metrics > /dev/null; done

在 Grafana 同一个面板上画两条曲线:

rate(request_count_total[1m])
irate(request_count_total[1m])

观察:哪条曲线更早反映出暴刷?哪条更平滑?把这幅图记在脑子里,以后选函数就不会错。

# 实验四 聚合练习

# 1. 数主机数
count(node_os_version)
# 2. 按系统版本分组统计
count_values("os_version", node_os_version)
# 3. 每台主机的内存总量
sum(node_memory_MemTotal_bytes) by (instance)
# 4. 全部主机内存总和
sum(node_memory_MemTotal_bytes)
# 5. 每台主机的 CPU 核心数
count(node_cpu_seconds_total{mode="system"}) by (instance)
# 6. 内存占用最多的前 3 台
topk(3, node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes)
# 7. /metrics 接口占总请求的百分比
sum(prometheus_http_requests_total{handler="/metrics", instance="localhost:9090"})
  / sum(prometheus_http_requests_total{instance="localhost:9090"}) * 100

# 实验五 故意犯错(很重要)

依次执行下面的错误写法,把报错信息抄下来:

# ① 范围向量直接画图
node_load1[5m]
# ② 正则没写 .*
node_cpu_seconds_total{mode=~"idle"}
# ③ 全空标签(应被拒绝)
{job=~".*"}
# ④ 占比分子分母粒度不同
prometheus_http_requests_total{code="200", handler="/-/reload", instance="localhost:9090"}
  / sum(prometheus_http_requests_total{handler="/-/ready", instance="localhost:9090"})

💡 认得报错,比背对语法更有价值。 真实故障现场,你看到的一定是报错信息。


# 3.12 下章预告

PromQL 会写了,但还有两个 "咽不下去" 的问题:

  1. 业务指标从哪来? 公司自研的 Java / Python 程序,既没有 /metrics ,也没有现成 Exporter—— 只能自己写一个。
  2. 标签为什么会错? 明明采集到了,Grafana 面板却是空的;想删掉没用的指标却不知道从哪下手。

下一章解决:

  • 用 Python / Go 亲手写一个 Exporter(40 行代码,看得懂就能改)
  • Grafana 面板自己动手做(不再依赖别人的模板)
  • 标签的完整生命周期: __address__ 、 __meta_* 是怎么一路变成 instance 的
  • relabel_configs 与 metric_relabel_configs :抓取前 vs 抓取后,差别到底在哪
  • 七个标签改写实战案例(含讲师在课堂上改四遍的那个经典例子)
  • 记录规则:给长表达式起个别名,还能顺便提速

本章作业:把 "七条必须带走的知识" 背下来,并且能不看笔记写出 CPU 使用率和内存使用率两条表达式。这两条是运维面试的 "入场券"。

此文章已被阅读次数:正在加载...更新于

请我喝[茶]~( ̄▽ ̄)~*

Jaxx.Wang 微信支付

微信支付

Jaxx.Wang 支付宝

支付宝