4. by 子句与 without 子句:详解 by 和 without 的用法,如何通过合理分组减少告警维度爆炸

在 PromQL 聚合查询中,by 和 without 是控制分组维度的核心子句。错误使用或滥用分组维度,是导致误差预算告警维度爆炸、告警风暴和存储成本飙升的主要根源。本章将深入剖析两者的语义差异、适用场景,并给出通过合理分组实现告警降噪的实战策略。

4.1 核心语义对比:by vs. without

特性 by 子句 without 子句
语义 显式指定保留哪些标签作为分组维度 显式指定移除哪些标签,其余标签自动成为分组维度
分组逻辑 只按 by 后列出的标签进行聚合 从原始标签集中排除 without 后列出的标签,剩余标签作为分组键
维度控制 精确控制输出时间序列的标签组合 适合快速移除高基数标签,保留其余维度
典型场景 按 service、team 等固定维度聚合 移除 instance、pod 等细粒度标签,保留上层维度
告警降噪效果 强(维度明确,不易爆炸) 中(需谨慎排除,避免意外保留高基数标签)
最佳实践:在误差预算告警规则中,优先使用 by 子句显式声明分组维度。这相当于白名单机制,能最大程度避免因标签结构变化导致的维度意外膨胀。

4.2 维度爆炸的根源:隐式分组与高基数标签

当聚合查询不指定 by 或 without 时,PromQL 默认按所有标签进行分组(相当于 without())。这在误差预算计算中极其危险:

  • 隐式全标签分组:例如 sum(rate(http_requests_total[5m])) 会按 method、path、status、instance 等所有标签组合生成大量时间序列。
  • 高基数标签污染:如果原始指标包含 user_id、request_id、pod 等基数极高的标签,聚合结果会瞬间膨胀到数千甚至数万条序列。
  • 告警规则连锁反应:每条聚合后的时间序列都会独立评估告警条件,导致告警数量呈指数级增长。
告警维度爆炸案例:某团队使用 avg by (service, instance) (rate(request_duration_seconds_count[5m])) 计算平均请求速率。由于 instance 标签包含 200 个 Pod IP,每次滚动更新后 IP 变化,导致告警规则评估出 200 条独立序列,触发 200 条重复告警。修复方案:移除 instance,改为 avg by (service),告警数量降至 3 条。

4.3 合理分组策略:从源头控制维度

以下策略可有效减少告警维度爆炸,同时保留必要的可观测性粒度:

  1. 显式白名单分组:始终使用 by 列出业务必需的维度标签,如 service、team、region。
  2. 移除高基数标签:使用 without(instance, pod, host) 排除细粒度基础设施标签。
  3. 分层聚合:先按细粒度聚合用于调试,再按粗粒度聚合用于告警。
  4. 标签降维:通过 label_replace 或 recording rule 将高基数标签映射为低基数标签(如将 pod 映射为 deployment)。
  5. 设置最大序列数:在 Prometheus 或 Thanos 中配置 promql.max-samples 和告警规则评估限制。

4.4 分组策略对比:by vs. without 在误差预算中的效果

查询示例 分组维度 输出序列数(假设) 告警降噪效果
sum(rate(errors_total[5m])) / sum(rate(requests_total[5m])) 所有标签(隐式) 500+ ❌ 极差(维度爆炸)
sum by (service) (rate(errors_total[5m])) / sum by (service) (rate(requests_total[5m])) service 5 ✅ 优秀(精确控制)
sum without (instance, pod) (rate(errors_total[5m])) / sum without (instance, pod) (rate(requests_total[5m])) 除 instance、pod 外的所有标签 20 ⚠️ 中等(依赖标签结构)
avg by (team, region) (slo_errors_budget_remaining) team、region 8 ✅ 优秀(业务维度聚焦)

4.5 分组与告警路由的协同设计

合理分组不仅减少告警数量,还能提升告警路由的准确性。以下 SVG 图展示了从原始指标到降噪告警的分组链路:

分组降噪链路:从原始指标到精准告警 原始指标 method, path, status, instance by(service) 分组聚合 service 维度聚合 误差预算计算 误差预算剩余 3 条时间序列 告警规则评估 3 条规则触发 降噪效果说明 • 原始指标:500+ 条时间序列(method × path × status × instance) • 分组后:3 条时间序列(仅 service 维度) • 告警数量:从 500+ 降至 3 条,降噪比 99.4% • 告警路由:按 service 直接路由到对应团队,无需人工过滤

4.6 实战案例:误差预算告警分组优化前后对比

以下案例展示了一个典型微服务架构中,通过优化分组维度实现告警降噪的完整过程:

阶段 查询语句 输出序列数 告警数量/天 MTTR(分钟)
优化前 avg by (service, instance, method) (rate(errors_total[5m])) 1,200 850 45
优化后 avg by (service) (rate(errors_total[5m])) 6 12 8
降噪效果 移除 instance 和 method 维度 ↓ 99.5% ↓ 98.6% ↓ 82.2%
关键结论:通过将分组维度从 by (service, instance, method) 缩减为 by (service),告警数量从日均 850 条降至 12 条。更重要的是,由于告警不再被 instance 变化和 method 细粒度干扰,值班团队能够更快定位到真正有问题的服务,MTTR 从 45 分钟降至 8 分钟。

4.7 分组维度设计检查清单

在编写或审查误差预算告警规则时,请对照以下清单:

  • 是否使用了 by 子句? 避免隐式全标签分组。
  • 分组维度是否包含高基数标签? 如 instance、pod、host、user_id。
  • 分组维度是否与告警路由匹配? 确保分组后的标签能直接映射到通知渠道或团队。
  • 是否有多余的维度? 每个额外维度都会线性增加序列数,检查是否真正需要。
  • 是否考虑了标签值变化? 如 Pod IP 滚动更新会导致维度膨胀,应使用 without(instance) 或映射为 deployment 标签。
  • 是否设置了告警规则评估限制? 在 Prometheus 配置中设置 evaluation_interval 和 max_samples 作为安。

4.8 进阶技巧:动态分组与标签映射

当无法直接移除高基数标签时,可以使用以下技巧实现动态降维:

# 使用 label_replace 将 pod 标签映射为 deployment 标签
- record: job:slo_errors_budget_remaining:by_deployment
  expr: |
    label_replace(
      avg by (service, pod) (slo_errors_budget_remaining),
      "deployment",
      "$1",
      "pod",
      "(.+)-[a-z0-9]+-[a-z0-9]+"
    )

# 然后按 deployment 分组聚合,避免 pod 维度爆炸
- alert: ErrorBudgetBurn
  expr: |
    avg by (service, deployment) (job:slo_errors_budget_remaining:by_deployment) < 0.1
  labels:
    severity: critical
  annotations:
    summary: "服务 {{ $labels.service }} 部署 {{ $labels.deployment }} 误差预算不足"
生产建议:在 recording rule 中完成标签映射和分组聚合,告警规则直接引用预聚合指标。这样既能保持告警规则的简洁性,又能通过 recording rule 的缓存机制提升查询性能。

通过合理运用 by 和 without 子句,结合标签降维和分层聚合策略,可以将误差预算告警的维度爆炸风险降至最低,实现从“告警风暴”到“精准告警”的质变。