Learn
Prometheus/07-promql-operators

PromQL 运算符

上一章我们学会了「把想要的序列挑出来」。这一章解决下一个问题:怎么把两组序列组合起来做运算。

错误率 = 错误数 ÷ 总数。内存使用率 = 已用 ÷ 总量。这些查询都需要让两个向量「配对」相除。听起来简单,但 PromQL 的运算规则和普通编程语言完全不同——它运算的对象不是两个数字,而是两组带标签的序列。

理解「向量匹配」是从 PromQL 入门到熟练的分水岭。

1. 运算符全景

分类运算符作用对象
算术+ - * / % ^标量↔标量、向量↔标量、向量↔向量
比较== != > < >= <=同上,默认「过滤」,加 bool 变「求值」
逻辑/集合and or unless只能向量↔向量
聚合sum avg max min count 等向量(下一章细讲,本章会用到)

优先级从高到低(同级从左到右,^ 例外是右结合):

1. ^
2. * / % atan2
3. + -
4. == != > < >= <=
5. and unless
6. or

不确定优先级时,加括号。PromQL 的括号是免费的,可读性收益巨大。

2. 算术运算符

2.1 向量 ↔ 标量:最简单的情况

标量会作用到向量的每一条序列上:

# 字节转 GB
node_memory_MemAvailable_bytes / 1024 / 1024 / 1024
 
# 秒转毫秒
rate(http_request_duration_seconds_sum[5m]) * 1000
 
# 比率转百分比
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100
 
# 取模:找出奇数编号的分片
kafka_partition_id % 2
ℹ️向量 ↔ 标量运算会保留所有标签
输入: node_memory_MemAvailable_bytes{instance="a", job="node"}  8589934592
表达式: node_memory_MemAvailable_bytes / 1024 / 1024 / 1024
输出: {instance="a", job="node"}  8

注意输出丢掉了指标名(__name__ 被删除),但保留了所有其他标签。这是 PromQL 的普遍规则:任何运算都会丢弃指标名,因为运算结果已经不是原来那个指标了。

2.2 向量 ↔ 向量:标签必须完全一致

这是关键规则:

两个向量做运算时,只有「标签集完全相同」的序列才会配对;配不上的序列直接从结果中消失。

看一个成功的例子:

左侧: node_memory_MemAvailable_bytes{instance="a", job="node"}  8589934592
      node_memory_MemAvailable_bytes{instance="b", job="node"}  4294967296
 
右侧: node_memory_MemTotal_bytes{instance="a", job="node"}     17179869184
      node_memory_MemTotal_bytes{instance="b", job="node"}     17179869184
 
表达式: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes
 
结果: {instance="a", job="node"}  0.5
      {instance="b", job="node"}  0.25

两边除了指标名之外标签完全一致,所以 a 配 a、b 配 b,各自相除。

再看一个失败的例子:

左侧: http_requests_total{job="api", status="500", method="GET"}   47
右侧: http_requests_total{job="api"}                             98260
 
表达式: 左 / 右
结果: 空!

因为左侧多了 status 和 method 两个标签,右侧没有,标签集不相同 → 无法配对 → 返回空。

这是新手最常遇到的「查询返回空」的原因。 解决办法有两个方向:用 sum 抹平标签差异,或者用 on / ignoring 显式指定匹配规则(见第 5 节)。

2.3 实战:几个常用的算术查询

# 内存使用率(%)
100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)
 
# 磁盘使用率(%)
100 * (
  1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"}
      / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"}
)
 
# CPU 使用率(%):100 减去 idle 的占比
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
 
# 平均请求耗时(毫秒)
1000 *
  rate(http_request_duration_seconds_sum[5m])
  / rate(http_request_duration_seconds_count[5m])
 
# 每个请求平均消耗多少 CPU 秒
rate(process_cpu_seconds_total[5m]) / rate(http_requests_total[5m])

最后一个查询很有意思——它把两个完全不同的指标结合起来,得到一个「单位成本」指标。当代码引入性能退化时,这个数字会先于 CPU 使用率发出信号(因为 CPU 使用率还受流量影响,而单位成本不受)。

3. 比较运算符

3.1 默认行为是「过滤」,不是「求值」

这是 PromQL 和普通编程语言最不一样的地方:

up == 0

在 Python 里 x == 0 返回 True 或 False。在 PromQL 里,它返回的是:

输入: up{job="api", instance="a"}  1
      up{job="api", instance="b"}  0
      up{job="web", instance="c"}  1
 
表达式: up == 0
 
输出: up{job="api", instance="b"}  0     ← 只剩满足条件的序列,值保持原样

比较运算符是一个过滤器:满足条件的序列原样保留(值不变、标签不变,包括指标名也保留),不满足的直接丢弃。

这个设计非常适合告警——告警规则的语义就是「表达式有结果就告警,没结果就不告警」:

- alert: InstanceDown
  expr: up == 0          # 有序列返回 = 有实例挂了 = 触发告警
  for: 5m
# 更多过滤示例
node_load1 > 4                                    # 负载超过 4 的机器
rate(http_requests_total[5m]) > 100               # QPS 超过 100 的序列
node_filesystem_avail_bytes < 10 * 1024^3         # 剩余空间不足 10GB
kafka_consumergroup_lag > 10000                   # 堆积超过 1 万条的分区

3.2 bool 修饰符:变成 0/1

加上 bool 关键字,语义就变成了「求值」——不过滤,把每条序列的值换成 0 或 1:

输入: up{instance="a"}  1
      up{instance="b"}  0
 
up == 0         →  {instance="b"} 0                     (过滤,1 条)
up == bool 0    →  {instance="a"} 0, {instance="b"} 1   (求值,2 条)

注意 bool 版本还会删除指标名(因为结果已经不是 up 了)。

bool 的典型用途一:算比例。

# 有多少比例的实例是健康的
avg(up == bool 1)
 
# 有多少比例的请求耗时超过 1 秒(配合 Summary 时有用)
avg(rpc_duration_seconds{quantile="0.5"} > bool 1)

bool 的典型用途二:画一条「状态线」。

不加 bool 的话,条件不满足时序列直接消失,Grafana 上会出现断线。加了 bool 就永远有值(0 或 1),能画出完整的状态阶梯图。

bool 的典型用途三:组合多个条件打分。

# 一个简单的健康评分:三个条件各占 1 分
  (up == bool 1)
+ (node_load1 < bool 4)
+ (node_memory_MemAvailable_bytes > bool 1024^3)
⚠️标量之间比较必须加 bool
2 > 1          # ❌ 报错:comparisons between scalars must use BOOL modifier
2 > bool 1     # ✅ 返回标量 1

原因是「过滤」对标量没有意义——一个标量要么留下要么消失,而 PromQL 的表达式必须有确定的返回类型。所以标量比较强制要求 bool。

3.3 向量之间的比较

两个向量比较时,同样遵循「标签必须一致才配对」的规则:

# 找出「可用内存 < 总内存的 10%」的实例
node_memory_MemAvailable_bytes < node_memory_MemTotal_bytes * 0.1

结果保留的是左侧的序列(值和标签都来自左侧)。

# 找出当前 QPS 低于 1 小时前一半的服务(流量骤降)
sum by (job) (rate(http_requests_total[5m]))
  < 0.5 * sum by (job) (rate(http_requests_total[5m] offset 1h))

3.4 链式比较的陷阱

# ❌ 想表达「1 < x < 5」,但结果不对
1 < node_load1 < 5

PromQL 从左到右求值:先算 1 < node_load1——注意左边是标量、右边是向量,这在语法上合法但语义混乱(结果保留右侧向量中大于 1 的序列)。然后再和 5 比较。

正确写法是用 and 连接两个独立条件:

node_load1 > 1 and node_load1 < 5

4. 逻辑与集合运算符

三个运算符:and(交集)、or(并集)、unless(差集)。它们只能用于向量与向量之间,且只看标签,不看值。

4.1 and:交集

返回:左侧向量中,「标签集在右侧也存在」的那些序列。
值和标签都来自左侧。
左侧: node_load1{instance="a"}  5.2
      node_load1{instance="b"}  0.3
      node_load1{instance="c"}  8.1
 
右侧: node_memory_low{instance="a"}  1
      node_memory_low{instance="c"}  1
 
node_load1 and node_memory_low
→ {instance="a"} 5.2
  {instance="c"} 8.1

最重要的用途:给告警加「附加条件」。

# 磁盘使用率 > 85% 且 剩余空间 < 50GB(避免大盘误报)
(100 * (1 - node_filesystem_avail_bytes / node_filesystem_size_bytes) > 85)
and
(node_filesystem_avail_bytes < 50 * 1024^3)
# 错误率 > 5% 且 QPS > 10(避免低流量误报)
(
  sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
  / sum by (job) (rate(http_requests_total[5m]))
) > 0.05
and
sum by (job) (rate(http_requests_total[5m])) > 10

这就是上一章反复强调的「比值型告警必须配绝对量下限」的实现方式。

4.2 or:并集

返回:左侧的全部序列 + 右侧中「标签集在左侧不存在」的序列。
左侧优先。
左侧: metric_a{instance="a"}  1
      metric_a{instance="b"}  2
 
右侧: metric_b{instance="b"}  99      ← 标签和左侧的 b 相同,被忽略
      metric_b{instance="c"}  3       ← 左侧没有 c,被纳入
 
metric_a or metric_b
→ {instance="a"} 1
  {instance="b"} 2      ← 来自左侧,右侧的 99 被丢弃
  {instance="c"} 3      ← 来自右侧

用途一:兜底默认值。

# 如果 my_metric 没有数据,返回 0 而不是空
my_metric or vector(0)

这在 Grafana 单值面板上特别有用——没有数据时显示「No data」很难看,显示 0 更友好。

用途二:合并多个来源的同类指标。

# 新旧两套埋点并存的过渡期
sum(rate(http_requests_total[5m])) or sum(rate(legacy_http_requests_total[5m]))

用途三:让告警在指标消失时也能触发。

# 服务挂了(up == 0)或者根本没有 up 指标(目标被删了)
up == 0 or absent(up{job="critical-api"})

4.3 unless:差集

返回:左侧向量中,「标签集在右侧不存在」的那些序列。
左侧: node_load1{instance="a"}  5.2
      node_load1{instance="b"}  0.3
      node_load1{instance="c"}  8.1
 
右侧: maintenance_mode{instance="c"}  1
 
node_load1 > 4 unless maintenance_mode
→ {instance="a"} 5.2       ← c 在维护中,被排除

核心用途:排除例外。

# 磁盘告警,但排除正在维护的机器
(node_filesystem_avail_bytes / node_filesystem_size_bytes < 0.1)
unless on(instance)
node_maintenance_mode == 1
# Pod 不健康告警,但排除正在滚动更新的 Deployment
kube_pod_status_ready{condition="false"} == 1
unless on(namespace, deployment)
kube_deployment_status_condition{condition="Progressing", status="true"} == 1
💡用一个「开关指标」做优雅的告警静默

比起在 Alertmanager 里配 silence,用 unless 在查询层就排除掉,有几个好处:

  • 静默状态本身是一个指标,可以被查询、被审计、被展示在大盘上
  • 可以做细粒度控制(按 instance、按 namespace)
  • 静默生效和失效都是自动的,不会忘记取消

实现方式:用一个 Pushgateway 指标或者一个简单的 exporter 暴露维护状态:

node_maintenance_mode{instance="10.0.1.5:9100", reason="kernel_upgrade"} 1

然后所有相关告警都加上 unless on(instance) node_maintenance_mode == 1。

4.4 三者对比

左侧 L: {a} {b} {c}        右侧 R: {b} {c} {d}
 
L and    R  →  {b} {c}               交集(值取左侧)
L or     R  →  {a} {b} {c} {d}       并集(重叠部分取左侧)
L unless R  →  {a}                   差集

用集合论的语言:and 是 ∩,or 是 ∪,unless 是 L − R。

5. 向量匹配:on / ignoring / group_left / group_right

现在进入本章的核心难点。

5.1 问题:标签不一致怎么办

回到第 2.2 节那个失败的例子:

# ❌ 返回空
  sum by (job, status) (rate(http_requests_total{status=~"5.."}[5m]))
/ sum by (job)         (rate(http_requests_total[5m]))

左侧序列带 job 和 status 两个标签,右侧只带 job。标签集不同 → 配不上 → 空。

PromQL 提供了两个关键字来解决这个问题:

关键字含义
on(l1, l2)只用列出的这些标签做匹配,其余标签忽略
ignoring(l1, l2)匹配时忽略列出的这些标签,其余标签都要一致

修正上面的查询:

# ✅ 只用 job 做匹配
  sum by (job, status) (rate(http_requests_total{status=~"5.."}[5m]))
/ on(job)
  sum by (job)         (rate(http_requests_total[5m]))

或者等价地:

# ✅ 忽略 status 标签
  sum by (job, status) (rate(http_requests_total{status=~"5.."}[5m]))
/ ignoring(status)
  sum by (job)         (rate(http_requests_total[5m]))

on 还是 ignoring? 经验:

  • 两边标签差异很大、只有少数几个共同标签 → 用 on(明确列出用哪些配对)
  • 两边标签基本一致、只差一两个 → 用 ignoring(明确列出忽略哪些)

on 更常用,因为它的行为不会因为新增标签而意外改变。

5.2 三种基数关系

PromQL 的向量匹配有三种「基数」(cardinality),这是理解 group_left 的前提:

一对一 (one-to-one):
    左侧每条序列 ↔ 右侧恰好一条序列
    → 默认行为,不需要任何额外关键字
 
多对一 (many-to-one):
    左侧多条序列 → 右侧同一条序列
    → 需要 group_left
 
一对多 (one-to-many):
    左侧一条序列 → 右侧多条序列
    → 需要 group_right

5.3 一对一:默认行为

node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes

每个 instance 的可用内存对应它自己的总内存,一一对应。这是最常见的情况,什么都不用写。

如果一对一匹配中出现了「一个左侧序列匹配到多个右侧序列」,Prometheus 会报错:

Error: found duplicate series for the match group {job="api"} on the right hand-side
of the operation: [...]; many-to-many matching not allowed:
matching labels must be unique on one side

看到这个报错,说明你需要的是多对一(或一对多),要加 group_left / group_right。

5.4 多对一:group_left

场景:一个「细粒度」向量除以一个「粗粒度」向量。

左侧(细): 每个 (job, status) 一条序列
    {job="api", status="500"}  3.2
    {job="api", status="502"}  0.8
    {job="api", status="503"}  0.1
 
右侧(粗): 每个 job 一条序列
    {job="api"}  120.5

左侧 3 条序列都要除以右侧那 1 条 → 多对一 → 用 group_left:

  sum by (job, status) (rate(http_requests_total{status=~"5.."}[5m]))
/ on(job) group_left
  sum by (job)         (rate(http_requests_total[5m]))

结果:

{job="api", status="500"}  0.0266
{job="api", status="502"}  0.0066
{job="api", status="503"}  0.0008

记忆方法:group_left 中的 "left" 指的是「多的那一侧在左边」。

group_left   →  左侧是「多」,右侧是「一」    左 N : 1 右
group_right  →  左侧是「一」,右侧是「多」    左 1 : N 右

5.5 group_left 的第二个能力:把右侧的标签「贴」过来

group_left 后面可以跟一个标签列表,表示从右侧向量额外复制这些标签到结果里:

group_left(label1, label2)

这是 PromQL 里极其实用的一个技巧,叫做**「info 指标关联」**。

场景:给指标补充元数据。

假设你有一个 info 指标记录了每个实例的版本、机房、负责团队:

myapp_build_info{instance="10.0.1.5:8080", version="2.3.1", region="bj", team="order"} 1
myapp_build_info{instance="10.0.1.6:8080", version="2.3.0", region="sh", team="order"} 1

而 QPS 指标上只有 instance:

http_requests_total{instance="10.0.1.5:8080", job="api"}  ...

想按版本看 QPS,就用 group_left 把 version 标签贴过来:

sum by (version) (
    sum by (instance) (rate(http_requests_total[5m]))
  * on(instance) group_left(version)
    myapp_build_info
)

逐层解释:

1. sum by (instance) (rate(...))     每个实例的 QPS
2. * on(instance) group_left(version) 用 instance 配对,从右侧带上 version 标签
   (乘以 1 —— info 指标的值恒为 1,所以数值不变,只是为了「借」标签)
3. sum by (version) (...)             按版本聚合

结果:

{version="2.3.1"}  85.2
{version="2.3.0"}  42.7
ℹ️为什么是「乘以 1」这么奇怪的写法

info 指标的值恒为 1,所以 x * 1 = x,数值完全不变。这个乘法的唯一目的是触发一次向量匹配,从而把右侧的标签复制过来。

这是 PromQL 社区的一个惯用法(idiom)。看起来别扭,但因为 PromQL 没有 SQL 那样的 JOIN 语法,这就是标准做法。

同样的技巧可以用在很多地方:

# 按 Kubernetes Deployment 看 Pod 的 CPU 使用
sum by (deployment) (
    rate(container_cpu_usage_seconds_total[5m])
  * on(pod) group_left(deployment) kube_pod_owner
)
 
# 按机房看错误率
sum by (region) (rate(http_requests_total{status=~"5.."}[5m]))

5.6 一对多:group_right

group_right 是 group_left 的镜像——用于「一的那侧在左边」的情况:

# 左侧一条(粗),右侧多条(细)
  sum by (job) (rate(http_requests_total[5m]))
/ on(job) group_right
  sum by (job, status) (rate(http_requests_total{status=~"5.."}[5m]))

实践中 group_right 用得少得多,因为你总可以把表达式左右调换,改用 group_left。而 group_left 更符合「左侧是主体,右侧是补充信息」的直觉。

建议:优先写成 group_left 的形式。 如果发现需要 group_right,先想想能不能把两边换个位置。

5.7 常见报错与排查

报错一:many-to-many matching not allowed

found duplicate series for the match group {job="api"} on the right hand-side

原因:右侧在匹配标签上不唯一。可能是:

  • 忘了写 group_left(本来就是多对一)
  • on() 里列的标签不够,导致右侧有多条序列落在同一个匹配组

排查方法:单独执行右侧表达式,看在 on() 指定的标签下是否唯一:

# 检查右侧是否每个 job 只有一条
count by (job) (你的右侧表达式)
# 如果结果 > 1,就是这里的问题

报错二:结果为空(不报错)

原因:两边在匹配标签上没有交集。

排查方法:分别执行两侧,对比标签值。

# 左侧有哪些 job
count by (job) (左侧表达式)
# 右侧有哪些 job
count by (job) (右侧表达式)
# 如果一个是 "api" 一个是 "api-server",那就找到原因了

报错三:multiple matches for labels(group_left 之后仍报错)

原因:加了 group_left 但两边都多,是真正的「多对多」。PromQL 不支持多对多。

解决:先用聚合把其中一侧变成一对一。

⚠️向量匹配的调试黄金法则

遇到向量匹配问题,永远先把表达式拆成两半单独执行:

# 第 1 步:单独跑左侧,记下返回的标签
sum by (job, status) (rate(http_requests_total{status=~"5.."}[5m]))
 
# 第 2 步:单独跑右侧,记下返回的标签
sum by (job) (rate(http_requests_total[5m]))
 
# 第 3 步:对比标签集
#   完全相同        → 直接运算,不需要 on/ignoring
#   左多右少        → on(共同标签) group_left
#   左少右多        → on(共同标签) group_right
#   有标签名不一致  → 先用 label_replace 改名

90% 的向量匹配问题在第 3 步就能定位。

6. 综合实战

6.1 完整的错误率查询

# 版本 1:全局错误率(最简单,不需要向量匹配)
sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
# 版本 2:按服务分组(两边标签一致,仍不需要 on)
sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
/
sum by (job) (rate(http_requests_total[5m]))
# 版本 3:按服务分组,同时展开状态码明细(需要 group_left)
  sum by (job, status) (rate(http_requests_total{status=~"5.."}[5m]))
/ on(job) group_left
  sum by (job)         (rate(http_requests_total[5m]))
# 版本 4:加上防误报条件和百分比转换
100 * (
    sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
  / sum by (job) (rate(http_requests_total[5m]))
)
and
sum by (job) (rate(http_requests_total[5m])) > 10
⚠️版本 2 有一个隐蔽的坑
sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
/ sum by (job) (rate(http_requests_total[5m]))

如果某个 job 完全没有 5xx(这是好事!),那么分子里就没有这个 job 的序列,除法配对失败,结果里这个 job 直接消失——而不是显示 0。

在 Grafana 大盘上的表现是:健康的服务反而不显示,只有出过错的服务才有线。很反直觉。

修正:给分子加一个 or 兜底。

(
  sum by (job) (rate(http_requests_total{status=~"5.."}[5m]))
  or
  sum by (job) (rate(http_requests_total[5m])) * 0
)
/
sum by (job) (rate(http_requests_total[5m]))

* 0 那一项构造出了「所有 job 的 0 值序列」,or 让它只在分子缺失时生效。

6.2 rate 比值:计算平均值和单位成本

# 平均请求耗时(Histogram 的 sum / count)
rate(http_request_duration_seconds_sum[5m])
/
rate(http_request_duration_seconds_count[5m])

这两个序列的标签完全一致(只有指标名不同),所以是标准的一对一匹配,不需要 on。

# 每个请求平均消耗的 CPU 时间
rate(process_cpu_seconds_total[5m])
/ on(instance, job)
rate(http_requests_total[5m])

这里需要 on,因为 http_requests_total 上还有 method、status 等标签。更好的写法是先聚合:

  sum by (instance) (rate(process_cpu_seconds_total[5m]))
/ sum by (instance) (rate(http_requests_total[5m]))

6.3 缓存命中率

sum by (cache) (rate(cache_operations_total{result="hit"}[5m]))
/
sum by (cache) (rate(cache_operations_total{op="get"}[5m]))

注意分子分母来自同一个指标的不同切片,标签在 by (cache) 之后完全一致,一对一匹配成功。

6.4 Apdex 分数

Apdex(Application Performance Index)是一个业界标准的满意度指标:

Apdex = (满意数 + 容忍数 / 2) / 总数
 
满意:耗时 ≤ T
容忍:T < 耗时 ≤ 4T
不满意:耗时 > 4T

设 T = 0.3 秒(那么 4T = 1.2 秒,取最近的桶 1 秒):

(
    sum(rate(http_request_duration_seconds_bucket{le="0.3"}[5m]))
  + sum(rate(http_request_duration_seconds_bucket{le="1"}[5m]))
) / 2
/
sum(rate(http_request_duration_seconds_count[5m]))

推导:满意数 = le="0.3" 的桶;满意+容忍 = le="1" 的桶。

(满意 + 容忍/2) = (满意 + (le1 - 满意)/2) = (满意 + le1) / 2

所以直接把两个桶相加除以 2 即可,非常优雅。


🎯练习 1:预测运算结果

给定以下数据:

向量 A:
  cpu_usage{instance="web-1", region="bj"}  85
  cpu_usage{instance="web-2", region="bj"}  30
  cpu_usage{instance="web-3", region="sh"}  92
 
向量 B:
  cpu_limit{instance="web-1", region="bj"}  100
  cpu_limit{instance="web-2", region="bj"}  100
  cpu_limit{instance="web-4", region="sh"}  100
 
向量 C:
  in_maintenance{instance="web-3"}  1

写出以下每个表达式的完整结果(含标签和值):

1.  cpu_usage / cpu_limit
2.  cpu_usage > 80
3.  cpu_usage > bool 80
4.  cpu_usage and cpu_limit
5.  cpu_usage or cpu_limit
6.  cpu_usage unless on(instance) in_maintenance
7.  cpu_usage > 80 unless on(instance) in_maintenance
8.  cpu_usage * 2
🎯练习 2:修复失败的向量匹配

下面 4 个查询都无法得到预期结果,请诊断并修正:

# 1. 想算:每个状态码占总请求的比例
sum by (status) (rate(http_requests_total[5m]))
/
sum(rate(http_requests_total[5m]))
 
# 2. 想算:每个 Pod 的内存使用率
container_memory_usage_bytes / container_spec_memory_limit_bytes
 
# 3. 想算:按 K8s Deployment 汇总的 CPU 使用量
sum by (deployment) (rate(container_cpu_usage_seconds_total[5m]))
 
# 4. 想算:排除测试环境后的错误率
sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
unless
http_requests_total{env="test"}
🎯练习 3:用 group_left 关联元数据

你的环境中有以下指标:

# 应用 QPS(只有 instance 和 job 标签)
http_requests_total{job="api", instance="10.0.1.5:8080", status="200"}
 
# 构建信息(info 指标,值恒为 1)
app_build_info{job="api", instance="10.0.1.5:8080", version="2.3.1", commit="a1b2c3", team="order"} 1
 
# 实例元数据
instance_meta{instance="10.0.1.5:8080", region="bj", az="bj-a", machine_type="c6.xlarge"} 1

完成以下需求:

  1. 按 version 分组统计 QPS,用于观察灰度发布的流量分配。
  2. 按 region 分组统计 5xx 错误率。
  3. 找出「运行着 2.3.0 旧版本」且「QPS 大于 10」的实例。
  4. 解释为什么 group_left 里的标签列表不能省略。
🎯练习 4:设计一套带静默机制的告警

为一个微服务集群设计告警,要求:

  1. 服务 5xx 错误率超过 2% 时告警,但要避免低流量误报。
  2. 正在进行滚动发布的服务不告警(发布状态由 deploy_in_progress 指标表示,值为 1)。
  3. 标记为 tier="experimental" 的服务用更宽松的阈值(10%)。
  4. 告警信息里要带上服务的负责团队(来自 service_meta 指标的 team 标签)。

已有指标:

http_requests_total{job, instance, status}
deploy_in_progress{job}  1
service_meta{job, team, tier}  1

写出完整的告警规则,并解释每个部分的设计考量。

小结

  • 算术运算(+ - * / % ^):向量↔标量作用于每条序列;向量↔向量要求标签集完全一致才能配对,配不上的序列消失。所有算术运算都会删除指标名。
  • 比较运算默认是过滤器(保留满足条件的序列,值和标签原样不变);加 bool 变成求值(全部保留,值变 0/1,删除指标名)。标量之间比较必须加 bool。
  • 逻辑运算 and(交集)/ or(并集,左侧优先)/ unless(差集)只看标签不看值,是告警加条件、加静默的主力工具。
  • 向量匹配是难点:用 on(...) 指定匹配用的标签,或 ignoring(...) 指定忽略的标签。
  • group_left / group_right 声明「多」在哪一侧;group_left(labels) 还能把右侧的标签复制到结果里,这是关联 info 指标(版本、团队、机房)的标准手法。
  • 遇到匹配问题,把表达式拆成两半单独执行,对比标签集——90% 的问题在这一步就能定位。
  • 「比值型」表达式在分子为空时整条序列会消失,用 or ... * 0 兜底。
  • 告警上线后要检查 /api/v1/rules 里的 health,向量匹配错误不会让 Prometheus 崩溃,只会让规则静静失效。