# 第 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 由两部分组成:
| 组成 | 内容 |
|---|---|
| key | Metric 名称 + Label 集合 + Timestamp(毫秒精度) |
| value | float64 格式的数值 |
📌 记住这个 "没有例外" 的事实: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 内置的函数式表达式语言,用于实时查找和聚合时间序列数据。
它的结果有三个去处:
- 图表展示(Grafana / 内置 Graph);
- 表格展示(表达式浏览器);
- 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 |
三条要点:
offset必须紧跟在选择器后面,中间不能断开;offset偏移的是时间基点,不是区间长度;- 既有范围又有偏移时:先定基点,再取范围。
[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:一表定生死
| 维度 | Histogram | Summary |
|---|---|---|
| 分位数在哪算 | 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 节讲
freevsavailable时,我们强调过 "只看 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 | == != <= < >= > |
| 5 | and unless |
| 6 | or |
规则:
- 同优先级左结合(
^例外,它是右结合); - 用
()改变次序 —— 永远推荐显式加括号,可读性远胜于背优先级表。
# 建议这样写,而不是依赖优先级 | |
(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 的计算方式与 "被平均" 问题
假设采集间隔 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 |
第 ④ 条拆解:
node_cpu_seconds_total{mode="idle"}[5m]—— 所有 CPU 所有核的空闲时间(range vector)irate(...)—— 每个核的空闲速率(instant vector)avg by (instance)(...)—— 把 "多核" 平均掉,得到 "这台机器的平均空闲率"(0~1)1 - ...—— 得到使用率(0~1)* 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])) |
三个使用要点:
- 必须配合
rate()(_bucket是 Counter,累加值不能直接算分位); - 必须用
_bucket序列,不是_sum/_count; - 结果只是估算值,精度取决于桶的划分。桶太少、太粗,误差会很大。
按维度分组算:
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 | =~ 是整值匹配 | 不写 .* 就匹配不到 |
| 3 | Counter 的总数没意义 | 必须配 rate / irate / increase |
| 4 | > 80 过滤、 > bool 80 转换 | 决定结果里 "有没有这条序列" |
| 5 | rate 被平均、 irate 抓瞬时 | 选错就看不到尖峰 |
| 6 | 算占比分子分母必须同粒度 | 否则空结果或 labelset 报错 |
| 7 | CPU 使用率用 avg by(instance) ,不是 sum | sum 结果会乘上核心数 |
# 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 会写了,但还有两个 "咽不下去" 的问题:
- 业务指标从哪来? 公司自研的 Java / Python 程序,既没有
/metrics,也没有现成 Exporter—— 只能自己写一个。 - 标签为什么会错? 明明采集到了,Grafana 面板却是空的;想删掉没用的指标却不知道从哪下手。
下一章解决:
- 用 Python / Go 亲手写一个 Exporter(40 行代码,看得懂就能改)
- Grafana 面板自己动手做(不再依赖别人的模板)
- 标签的完整生命周期:
__address__、__meta_*是怎么一路变成instance的 relabel_configs与metric_relabel_configs:抓取前 vs 抓取后,差别到底在哪- 七个标签改写实战案例(含讲师在课堂上改四遍的那个经典例子)
- 记录规则:给长表达式起个别名,还能顺便提速
本章作业:把 "七条必须带走的知识" 背下来,并且能不看笔记写出 CPU 使用率和内存使用率两条表达式。这两条是运维面试的 "入场券"。
