4、分组聚合:by 与 without 子句的语义与最佳实践

在 PromQL 聚合查询中,by 和 without 子句决定了聚合后保留哪些标签维度。理解它们的语义差异,是编写高效、准确告警规则的基础,也是排查自愈剧本触发延迟的关键环节——错误的维度保留可能导致告警被错误分组或遗漏。

4.1 核心语义对比

子句 语义 保留的标签 典型场景
by (<label_list>) 按指定标签分组聚合 仅保留列表中声明的标签 按 instance、job 聚合
without (<label_list>) 排除指定标签,保留其余所有标签 除列表外的全部标签 聚合时保留 job、namespace 等上下文
关键区别:by 是白名单模式,只保留你明确列出的标签;without 是黑名单模式,只移除你列出的标签,其余全部保留。在指标维度较多时,without 能避免遗漏重要上下文。

4.2 执行流程对比

以下 SVG 展示了 by(instance) 与 without(instance) 对同一组原始时间序列的处理差异:

by vs without 聚合流程对比 原始时间序列(4条) http_requests_total{job="api", instance="10.0.0.1:8080", method="GET", status="200"} http_requests_total{job="api", instance="10.0.0.2:8080", method="GET", status="200"} http_requests_total{job="api", instance="10.0.0.1:8080", method="POST", status="201"} http_requests_total{job="api", instance="10.0.0.2:8080", method="POST", status="201"} sum by(instance) 保留标签:instance 丢弃标签:job, method, status 结果:2条时间序列(按 instance 聚合) sum without(instance) 保留标签:job, method, status 丢弃标签:instance 结果:4条时间序列(按 job+method+status 聚合) 最佳实践提示 • 当需要按特定维度(如 instance)查看聚合结果时,使用 by • 当需要保留大部分上下文标签,仅排除少数维度时,使用 without • 在自愈剧本中,错误使用 by 可能导致告警丢失关键标签(如 namespace),造成触发延迟

4.3 自愈剧本中的典型陷阱

告警触发延迟案例:某自愈剧本使用 sum by(instance) 聚合 CPU 使用率,但原始指标包含 namespace 标签。聚合后 namespace 被丢弃,导致告警无法区分不同命名空间的实例,触发条件被错误合并,延迟了故障隔离。

以下表格总结了常见误用场景及修正方案:

误用写法 问题 修正写法
sum by(instance) (rate(...)) 丢弃了 namespace、job 等上下文 sum without(cpu) (rate(...))
avg without(job) (...) 意外保留了过多标签,结果维度爆炸 avg by(instance, namespace) (...)
count by() (...) 空标签列表导致聚合为单条序列,丢失所有维度 count by(severity) (...)

4.4 最佳实践清单

  • 优先使用 without 保留上下文:在自愈剧本中,告警规则通常需要保留 namespace、job、pod 等标签,以便精准定位故障源。使用 without 排除无关维度(如 cpu、device)更安全。
  • 明确列出 by 的必要标签:如果确定只需要按 instance 和 job 聚合,显式写出 by(instance, job),避免隐式依赖。
  • 避免空标签列表:by() 或 without() 会导致聚合为单条时间序列,通常不是预期行为。
  • 测试聚合结果:在 Prometheus 的 Graph 或 Console 视图中,先执行不带聚合的查询,确认标签维度,再添加 by/without。

4.5 性能考量

分组聚合的性能与保留的标签基数直接相关:

标签基数 聚合结果数量 性能影响
低(< 100) 少量时间序列 快速,内存开销小
中(100 - 1000) 中等数量 可接受,注意查询范围
高(> 1000) 大量时间序列 可能造成 OOM 或查询超时
优化建议:在自愈剧本中,如果 by 子句导致高基数标签(如 instance 包含大量动态 IP),考虑先使用 __name__ 或 job 缩小范围,再进行分组聚合。例如:sum by(instance) (rate(node_cpu_seconds_total{job="node-exporter"}[5m]))。