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 合理分组策略:从源头控制维度
以下策略可有效减少告警维度爆炸,同时保留必要的可观测性粒度:
- 显式白名单分组:始终使用
by列出业务必需的维度标签,如service、team、region。 - 移除高基数标签:使用
without(instance, pod, host)排除细粒度基础设施标签。 - 分层聚合:先按细粒度聚合用于调试,再按粗粒度聚合用于告警。
- 标签降维:通过
label_replace或 recording rule 将高基数标签映射为低基数标签(如将pod映射为deployment)。 - 设置最大序列数:在 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 图展示了从原始指标到降噪告警的分组链路:
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 子句,结合标签降维和分层聚合策略,可以将误差预算告警的维度爆炸风险降至最低,实现从“告警风暴”到“精准告警”的质变。