
1. 项目概述PromQL集合操作的核心价值在监控告警和可观测性领域Prometheus 的查询语言 PromQL 是数据分析的基石。很多朋友刚开始用 PromQL可能只停留在简单的rate()、sum()这类聚合函数上觉得够用了。但当你需要处理更复杂的监控场景比如“找出所有运行中但未在服务发现中注册的实例”或者“对比两个不同维度的指标集合找出它们的交集或差集”时仅仅靠聚合和过滤就显得力不从心了。这时候PromQL 的集合操作符and、or、unless就派上了大用场。它们处理的是向量与向量之间的关系实现一对多、多对多的逻辑匹配是构建精准、高效告警规则和仪表盘查询的关键。简单来说你可以把 PromQL 返回的每个时间序列一个带标签的指标数据点集合想象成一个“向量”。集合操作就是对这些向量集合进行类似数学集合的“与”、“或”、“非”运算。但 PromQL 的巧妙之处在于它运算的不是简单的数值而是基于时间序列的标签进行匹配。and求交集or求并集unless求差集。理解它们尤其是理解其背后的“向量匹配”规则能让你从写“能跑”的查询进阶到写“高效且意图明确”的查询。这不仅仅是语法糖更是提升监控系统洞察力的核心技能。2. 集合操作符基础与向量匹配机制在深入一对多/多对多场景前我们必须先夯实基础理解 PromQL 中向量如何进行比较和匹配。这是所有集合操作乃至所有二元操作的底层逻辑。2.1 向量匹配的两种模式PromQL 在进行两个向量之间的操作时比如vector1 and vector2并不是简单粗暴地比较数值或时间戳而是通过它们的标签集来寻找匹配项。这主要分为两种模式一对一匹配这是默认行为也是最严格的一种。它要求参与操作的左右两个向量中的元素其所有标签键值对必须完全一致才能被认为是匹配的并进行后续操作。示例up{jobnode-exporter} 1。这里up指标本身带有job、instance等标签。这个表达式是向量与标量的比较不涉及向量匹配。但对于metric_a{envprod} and metric_b{envprod}如果metric_a和metric_b都有完全相同的标签比如{envprod, instancehost1}它们就会成功匹配。多对一 / 一对多匹配这是我们本次要讨论的核心。它允许一个向量中的单个序列与另一个向量中的多个序列进行匹配。这通过group_left或group_right修饰符来实现。group_left表示左侧向量可以有“多”个元素匹配右侧向量的“一”个元素。结果向量将保留左侧多的一方的所有标签。group_right与group_left相反表示右侧向量可以有“多”个元素匹配左侧向量的“一”个元素。结果向量将保留右侧多的一方的所有标签。关键匹配时会忽略on或ignoring子句中指定的标签仅基于这些子句定义的标签子集进行匹配。2.2 标签操作子句on()与ignoring()为了在集合操作中更灵活地控制匹配的维度PromQL 提供了两个关键子句on (label1, label2, ...)仅使用括号内指定的标签进行匹配。其他标签在匹配时被忽略。这常用于在两个不同但相关的指标间建立关联。例如kube_pod_container_status_ready和kube_pod_container_resource_requests可能都有namespace、pod、container标签但后者可能多一个resource标签。用on(namespace, pod, container)可以精确地在容器维度关联就绪状态和资源请求。ignoring (label1, label2, ...)忽略括号内指定的标签使用剩下的所有标签进行匹配。这常用于去掉一些干扰维度比如instance、job或者某个指标特有的标签。例如比较不同副本的同一个应用指标时你可能想忽略replica标签只根据app、method等标签进行聚合或比较。注意on和ignoring在集合操作and,or,unless中的行为与在算术运算符中完全一致。它们定义了匹配的“键”。2.3 集合操作符的基本定义在明确匹配规则后我们看下三个操作符在匹配成功后的逻辑vector1 and vector2交集。产生一个由vector1中那些在vector2中存在匹配标签的元素组成的向量。vector1的样本值被保留。它用于过滤。例如up{jobapi} 1 and node_cpu_seconds_total{modeidle} 100找出所有api任务中up状态为1且CPU空闲时间小于100秒的实例匹配instance标签。vector1 or vector2并集。产生一个包含vector1的所有原始元素以及vector2中所有在vector1里没有匹配标签的元素的向量。它用于合并。例如合并来自两个不同数据源或采集任务的相同指标。vector1 unless vector2差集。产生一个由vector1中那些在vector2中没有匹配标签的元素组成的向量。它用于排除。经典用例up{jobnode-exporter} 1 unless on(instance) (up{jobnode-exporter} offset 5m) 1可以找出在过去5分钟内新上线或刚恢复的节点需要结合offset。3. 一对多/多对多匹配的实战解析基础概念清晰后我们进入核心实战环节一对多和多对多匹配。这是集合操作最强大也最容易出错的地方。3.1 典型场景指标与元信息的关联这是最常用的一对多场景。假设我们有两个指标service_up{serviceuser-api, instancehost1:8080, envprod, regionus-east} 1service_info{serviceuser-api, versionv1.2.3, ownerteam-a, deployed_at20231001}注意service_info可能没有instance标签因为它描述的是服务本身的元信息而不是每个实例的状态。但我们想为每个service_up的实例附上其对应的服务元信息如版本、负责人。错误尝试service_up 1 and service_info。这会失败因为标签不匹配左边有instance右边没有。正确做法使用group_left进行一对多匹配。service_up 1 and on(service) group_left(version, owner) service_infoon(service)我们只根据service标签进行匹配。忽略instance、env、region等。group_left(version, owner)这声明了匹配模式。因为service_info对于同一个service通常只有一条记录“一”而service_up可能有多个实例“多”所以是“多对一”使用group_left。括号内的version, owner指定了要从右侧向量service_info额外添加到结果向量中的标签。结果返回所有service_up 1的时间序列并且每个序列都新增了versionv1.2.3和ownerteam-a标签。这样你在 Grafana 表格里就能同时看到实例状态和它的版本信息。3.2 多对多场景复杂维度关联多对多匹配更复杂它通常隐含在查询中。假设我们监控一个分布式任务调度系统task_success_count{jobscheduler, task_typedata_export, useralice, queuehigh} 5task_resource_limit{jobscheduler, task_typedata_export, useralice, resourcecpu} 2task_resource_limit{jobscheduler, task_typedata_export, useralice, resourcememory} 4096现在我们想找出那些成功次数大于0且存在资源限制定义的(task_type, user)组合。注意task_success_count可能没有resource标签而task_resource_limit有resource标签这使得一个(task_type, user)组合在右边可能对应多条记录CPU、内存两条。查询task_success_count 0 and on(job, task_type, user) group_right(resource) task_resource_limiton(job, task_type, user)我们根据这三个标签进行匹配。group_right(resource)这里对于左边一个特定的(job, task_type, user)组合右边可能有多个resource对应的task_resource_limit序列。因此是“一对多”且“多”在右边所以用group_right。resource标签将从右侧添加到结果中。结果返回一个向量其中包含所有满足task_success_count 0的(job, task_type, user)组合并且因为匹配到了右侧多条记录结果会被“展开”。例如如果(data_export, alice)成功了5次且定义了CPU和内存限制那么结果会得到两条序列标签分别为{jobscheduler, task_typedata_export, useralice, resourcecpu}和... resourcememory值都是左边task_success_count的值5。实操心得多对多匹配的结果基数可能会爆炸式增长笛卡尔积的一部分务必谨慎使用尤其是在指标基数不同标签组合的数量很高的场景下可能对 Prometheus 查询引擎造成压力。通常你需要非常清楚on()子句定义的匹配键确保它是你真正想要的关联维度。3.3unless在过滤中的高级用法unless常用于“黑名单”或“异常检测”场景。一个高级用法是结合group_left实现条件排除。场景我们有一个核心应用app-core它依赖多个后端服务svc-a,svc-b,svc-c。我们监控所有服务的错误率error_rate{service~svc-.*}。但我们知道当svc-b进行计划内维护时它的错误率飙升是预期的不应触发核心应用的告警。我们想查询除了正处于维护期的服务外其他所有依赖服务的错误率。首先我们有一个标记服务维护状态的指标可能来自人工注入或运维系统service_maintenance{servicesvc-b, reasonupgrade} 11表示在维护查询error_rate{service~svc-.*} unless on(service) group_left(reason) (service_maintenance 1)error_rate{service~svc-.*}获取所有服务的错误率。service_maintenance 1找出所有状态为“正在维护”的服务。on(service)根据service标签匹配。group_left(reason)这是一个“多对一”匹配多个错误率序列对应一个维护状态序列这里需要仔细分析。实际上error_rate对于每个service可能只有一个序列假设没有其他维度service_maintenance对于每个service也只有一个序列。所以更接近一对一。但group_left(reason)的用意是如果匹配成功即服务在维护我们希望在结果中保留来自右侧的reason标签吗不unless的结果是保留左侧匹配失败的那些元素。group_left在这里的语义是“当执行匹配时允许左侧多对一并且如果匹配可以从右侧获取额外标签”。然而对于unless匹配成功的左侧元素会被丢弃。因此group_left子句在这里主要影响匹配逻辑对于最终保留的结果元素其标签集来自左侧error_rate不会添加reason。这个查询的最终效果是从所有服务的错误率列表中剔除掉那些service标签在service_maintenance 1结果集中存在的序列。这个例子展示了如何用unless实现基于另一个查询结果的动态过滤比在error_rate后硬编码service!svc-b更灵活和可维护。4. 性能调优与避坑指南集合操作很强大但使用不当极易引发性能问题或查询错误。4.1 性能影响与优化建议操作类型潜在性能影响优化建议and/unless中等。需要计算两个向量的标签匹配。如果左侧向量很大右侧向量也很大匹配计算开销会增大。尽量在操作前使用标签选择器或聚合函数减少向量大小。例如先sum by (service) (errors)再and比直接用原始高基数指标and要好。or较低。本质是合并但需要去重基于标签集。确保合并的向量来自相似的基数水平避免一个极大一个极小造成结果基数陡增。group_left/group_right高。这是性能风险最高的部分尤其是多对多匹配可能导致结果基数成倍增长最坏情况是笛卡尔积。1.严格限定on()子句只包含必要的匹配标签避免使用ignoring()忽略太多标签导致意外匹配。2.评估结果基数先用count()等聚合函数估算左右向量的基数以及匹配后可能产生的结果数量。3.避免在告警规则中滥用复杂的多对多集合操作可能使告警规则评估变慢影响告警时效性。4.2 常见错误与排查技巧错误“multiple matches for labels: many-to-one matching must be explicit (group_left/group_right)”原因你进行了一个操作可能是算术操作如*、/也可能是集合操作Prometheus 检测到存在潜在的多对一匹配关系但它不确定你意图保留哪边的标签。解决仔细分析你的数据模型。如果你确实需要一对多/多对一匹配必须显式添加group_left或group_right修饰符。如果你期望的是一对一匹配请检查左右两个向量的标签是否真的完全一致或者通过on()指定足够多的标签来确保唯一匹配。查询结果为空但感觉数据应该存在排查步骤单独执行左右表达式分别运行vector1和vector2部分确认它们各自能返回数据。检查标签一致性对比两个结果集中你期望用于匹配的标签键值对是否完全相同。特别注意空格、大小写、标签名拼写。检查时间戳对齐PromQL 在执行操作时要求左右向量中的匹配序列在完全相同的时间戳上才有结果。使用offset、不同采集周期或 recording rule 生成时间戳不一致的数据可能导致匹配失败。可以尝试使用avg_over_time(vector1[5m])等函数进行平滑或检查数据时间线。确认操作逻辑对于and是取交集。可能你的数据确实没有同时满足两边条件的时刻。对于unless是取差集可能你用来排除的集合恰好包含了全部。使用or合并后图表出现不连续的断点原因or合并的是时间序列而不是在每个时间点填充数值。如果序列 A 在时间点 T1 有值序列 B 在 T1 没值可能因为实例下线、指标不存在那么合并后的结果在 T1 点只有序列 A 的值。在 Grafana 中如果查询的是sum(metric_a or metric_b)而两个指标的标签集不同sum可能会因为某些时间点缺少某个序列而导致总和突变。解决这不是or的 bug而是数据本身的特性。如果需要更平滑的合并考虑使用coalesce()函数它可以为缺失的数据提供一个默认值。例如coalesce(metric_a, 0) coalesce(metric_b, 0)。group_left导致标签冲突场景metric_a{namea} and on(id) group_left(name) metric_b{nameb}问题左右向量都有一个叫name的标签但值不同。group_left指定从右侧获取name标签这会与左侧原有的name标签冲突。结果Prometheus 会报错因为无法决定保留哪个name标签。解决在group_left子句中不要引入与左侧向量冲突的标签名。如果右侧的标签是必须的但名称冲突可以考虑先使用label_replace函数重命名右侧的标签。例如label_replace(metric_b, right_name, $1, name, (.*))然后再用group_left(right_name)。5. 综合实战案例构建一个服务健康全景视图假设我们有一个微服务系统监控数据如下http_requests_total{servicecart, instancei1, endpoint/add, status200}http_requests_total{servicecart, instancei2, endpoint/add, status500}service_metadata{servicecart, versionv2.1, teamcheckout, slap99200ms}up{jobcart-service, instancei1} 1up{jobcart-service, instancei2} 0实例 i2 宕机probe_success{modulehttp_2xx, targetcart.service.com} 0外部探测失败目标创建一个查询展示每个服务的服务元数据版本、团队、SLA。实例健康状态up。最近5分钟的错误率非200状态码比率。外部探测状态。并且只显示那些至少有一个实例是健康的服务。分步构建查询步骤1获取每个服务的错误率# 计算每个服务、每个实例、每个端点的错误率非2xx/3xx视为错误这里简单以status!200为例 sum by (service, instance) ( rate(http_requests_total{status!200}[5m]) ) / sum by (service, instance) ( rate(http_requests_total[5m]) )假设这个查询结果命名为error_rate_by_instance。步骤2关联服务元数据# 将错误率与元数据关联使用一对多匹配一个服务对应多个实例 error_rate_by_instance and on(service) group_left(version, team, sla) service_metadata现在每个实例的错误率序列都带上了version,team,sla标签。步骤3关联实例健康状态# 关联 up 指标。注意 up 的标签是 job 和 instance我们的错误率有 service 和 instance。 # 我们需要知道 job 和 service 的对应关系假设 job 标签包含服务名或者我们通过 relabel 知道 cart 服务的 job 就是 cart-service。 # 这里假设我们已经通过某种方式统一了标签或者使用 on(instance) 匹配。 # 更稳健的做法是up 指标也能暴露 service 标签通常通过服务发现实现。 # 假设理想情况下up 指标也有 service 标签 up{servicecart}为了简化我们假设up指标通过 Prometheus 的服务发现自动获得了service标签。那么我们可以直接和上一步的结果进行and操作过滤出健康的实例( error_rate_by_instance and on(service) group_left(version, team, sla) service_metadata ) and on(service, instance) (up 1)这个查询结果给出了所有健康实例的错误率及服务元数据。步骤4关联外部探测状态# 外部探测指标 probe_success其 target 标签可能包含服务域名。 # 我们需要建立 target 到 service 的映射。这通常比较困难可能需要 label_join 或 recording rules 预处理。 # 假设我们有一个 recording rule 或已知映射使得 probe_success 也有 service 标签。 # 我们只关心探测失败的服务。 probe_success{servicecart} 0步骤5最终整合与过滤我们想要一个列表列出所有至少有一个健康实例的服务并显示其元数据、任意一个健康实例的错误率或最大错误率、以及外部探测状态。这需要用到聚合和or来合并信息。一个相对综合的查询可能如下概念性可能需要根据实际标签调整# 首先获取有健康实例的服务列表通过 max(up) by (service) 1 # 然后将这些服务的元数据、错误率取最大值、探测状态合并起来。 # 1. 服务健康状态标识 (max(up) by (service) 1) * 1 # 将布尔值转为1/0方便后续作为因子 # 2. 服务元数据取一个样本即可 max by (service) (service_metadata) # 假设元数据值一样用 max 取一个 # 3. 服务最大错误率在所有实例中 max by (service) ( error_rate_by_instance and on(service, instance) (up 1) # 只考虑健康实例 ) or vector(0) # 如果没有错误率数据则用0代替 # 4. 外部探测状态失败为1成功为0 (probe_success 0) * 1 or vector(0) # 转为1/0缺数据为0 # 最终我们可以通过 label_join 或直接在 Grafana 表格中查看这些并列的指标。 # 一个更高级的做法是使用 PromQL 的 group 函数返回1和 label_replace 来构造一个复合指标但这非常复杂。 # 在实践中更常见的做法是在 Grafana 中创建 Table 面板使用多个查询分别获取上述数据然后通过“Transform”功能如“Merge”、“Organize fields”将它们关联起来这比在 PromQL 中做极其复杂的连接要直观和高效得多。这个案例说明了虽然and、or、unless和group_left/right非常强大可以构建复杂的逻辑关联但在面对多源异构数据、需要生成人类可读的汇总视图时PromQL 可能不是唯一的工具甚至不是最合适的工具。将复杂的关联逻辑拆分部分在 PromQL 中完成部分在可视化层如 Grafana或数据预处理层如 Recording Rules完成往往是更可维护和高效的选择。理解集合操作的本质是理解标签匹配。它让你能像操作数据库表一样通过“标签外键”来连接不同的时间序列。从简单的过滤 (and)到数据合并 (or)再到条件排除 (unless)再到强大的维度关联 (group_left/right)这套组合拳能解决监控中大部分的数据关联和筛选问题。真正的挑战不在于记住语法而在于精确地理解你的数据模型——每个指标携带哪些标签它们之间通过哪些标签可以产生有意义的关联。我个人的经验是在编写复杂的集合查询前先用简单的选择器把左右两边的数据单独查出来仔细对比标签画一画你期望的匹配关系图这能避免很多后期的调试时间。