聚合与向量匹配 · by/without 与那些一对多
sum by (job) 里的 by 到底在干什么?两个指标相除时标签怎么配对?这一章讲清聚合的「丢标签」本质、向量匹配的一对一/多对一规则,以及 on/ignoring/group_left 这几个最让人头疼的关键字。
聚合与向量匹配 · by/without 与那些一对多
rate() 之后,你几乎总要聚合:把几百条 series 合并成几条有意义的。这一章讲聚合,以及它的孪生难题——两个向量怎么配对。
聚合的本质:丢标签
sum by (job) (x) 干的事,本质是:按 job 分组,丢掉其他所有标签,组内求和。
输入(6 条 series):
{job="api", instance="1", status="200"} 10
{job="api", instance="2", status="200"} 20
{job="api", instance="1", status="500"} 1
{job="db", instance="3", status="200"} 30
{job="db", instance="4", status="200"} 40
{job="db", instance="3", status="500"} 2
sum by (job) (x) → {job="api"} 31 ← instance 和 status 都没了!
{job="db"} 72关键理解:聚合结果的标签集 = by 里列的那些,仅此而已。 所有没被 by 提到的标签全部消失。
without 是反过来的:列出要丢掉的标签,其余全保留。
sum without (instance) (x) → {job="api", status="200"} 30
{job="api", status="500"} 1
{job="db", status="200"} 70
{job="db", status="500"} 2实战建议:优先用 without。 为什么?因为 by 是白名单——将来加了新标签(比如 region),by (job) 会默默把它丢掉,你可能根本没意识到结果被跨 region 混在一起了。而 without (instance) 是黑名单,新标签会自动保留,更符合“我只想合并掉实例维度”的意图。
这条经验很多人不知道,但它能避免真实的误导。
聚合是降基数最有效的手段
第 2 部分讲基数时说过 series 数是乘法**。聚合是那个乘法的逆运算——它把维度消掉。**
原始: 5(method) × 50(route) × 8(status) × 12(le) × 20(instance) = 480,000
sum by (le): = 12 ← 消掉了 4 个维度!这就是 recording rules 的价值:把常用的聚合结果预先算好存下来,查询时直接读那 12 条,而不是每次都从 48 万条里现算。第 5 部分会展开。
一个实用心法:聚合时问自己“我丢掉的这个维度,真的不需要吗?” 丢 instance 通常没问题(你要看服务整体);但如果你正在排查“是不是某个坏实例”,丢掉 instance 就等于丢掉了答案。聚合是有损的,损失的正是你埋点时费心保留的提问自由度。
向量匹配:两个指标怎么配对
现在是难的部分。a / b 时,PromQL 怎么知道 a 的哪条 series 该除以 b 的哪条?
默认规则(一对一):标签集完全相同的两条 series 配对**。**
a: {job="api", instance="1"} 100
{job="api", instance="2"} 200
b: {job="api", instance="1"} 10
{job="api", instance="2"} 20
a / b → {job="api", instance="1"} 10 ← 标签全同 → 配对
{job="api", instance="2"} 10注意:结果会丢掉指标名(__name__),因为“a 除以 b”是个新东西,不再是 a 也不是 b。
问题来了:如果标签不完全相同呢? 比如 a 有个额外的 status 标签,b 没有——默认规则下,一条都配不上,结果是空。
这就是“我的查询返回空但每一半都有数据”这个经典困惑的根源。 解法是 on / ignoring:
# 只按 job 配对,忽略其他标签
a / on (job) b
# 忽略 instance,按剩下的配对
a / ignoring (instance) bon 是白名单(只看这些标签),ignoring 是黑名单(忽略这些标签)——和 by/without 的关系一样。
一对多:group_left / group_right
最让人头疼的场景:一边多、一边少。
经典例子:你想给指标打上元数据标签(比如从 kube_pod_info 拿 node 名):
# container_cpu 有很多条(每个 pod 一条)
# kube_pod_labels 也是每个 pod 一条,但带着额外的标签
container_cpu_usage_seconds_total * on (pod) group_left(node) kube_pod_labelsgroup_left 的意思是:“左边是多的那一侧”——即左边的多条 series 可以匹配右边的同一条。group_left(node) 里的 node 是“从右边额外拿过来的标签”。
记忆口诀:group_left = 左边多、从右边捞标签;group_right 反之。
两个常见的坑:
- “multiple matches for labels”错误 —— 说明你方向搞反了,或者匹配的标签不够唯一(右边有多条 series 匹配同一个左边的)。检查:
on()里的标签组合在“一”的那侧是否真的唯一。 - 忘了
group_left的括号参数 ——group_left不带参数只是“允许多对一”,不会把右边的标签带过来。要带标签必须写group_left(node)。
这个 xxx * on(...) group_left(...) yyy 的模式是生产环境的高频套路(给指标补元数据、做 join),值得专门记住。
实战:几个高频模式
# ① 错误率(经典 RED)—— 注意分子分母标签要能配对
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/
sum(rate(http_requests_total[5m])) by (service)
# ② 给指标补元数据(一对多)
node_cpu_seconds_total * on (instance) group_left(node_type) node_metadata
# ③ 占比:每个 pod 占总量的比例(注意 on() 空括号)
container_memory_usage_bytes
/ on () group_left
sum(container_memory_usage_bytes)
# ④ 找出"存在于 A 但不在 B"的 series
up == 1 unless on (instance) some_other_metric模式 ③ 里的 on ()(空括号)值得注意:它表示“不按任何标签配对”,即所有左边的 series 都匹配右边那唯一一条**(sum() 的结果没有标签)。这是“算占比”的标准写法。**
模式 ④ 的 unless 是集合运算:PromQL 有 and(交)、or(并)、unless(差)。它们也遵循向量匹配规则,同样可以配 on/ignoring。unless 特别适合“找出缺失的东西”——比如“哪些实例 up 了但没上报某个指标”。
小结与下一章
聚合的本质是丢标签:sum by (job) = 按 job 分组、丢掉其他所有标签、组内求和;without 是黑名单版。实战优先用 without——by 是白名单,将来加了新标签会被默默丢掉(可能让结果跨维度混在一起而你不知道);without 会自动保留新标签。聚合是基数乘法的逆运算,消维度(48 万 → 12 条),这正是 recording rules 的价值;但聚合有损——丢掉的维度可能正是答案。向量匹配默认是标签集完全相同才配对(结果丢 __name__)——这是“两半都有数据但结果为空”的根源,解法是 on(白名单)/ignoring(黑名单)。一对多用 group_left(左边多、从右边捞标签;group_left(node) 的括号参数才是“带过来的标签”);报 “multiple matches” 说明方向反了或“一”的那侧不唯一。高频模式:错误率、补元数据、on() 空括号算占比、unless 找缺失。
分位数查询有它自己的一堆坑。下一章看 histogram_quantile()。👉 直方图查询。
参考资料
- [1] Prometheus 官方文档:Operators — Aggregation operators、Vector matching。
- [2] Prometheus 源码
promql/engine.go(向量匹配实现)、promql/parser/ast.go(AggregateExpr/BinaryExpr)。 - [3] 关联基数乘法、RED 查询、recording rules。