recording rules · 预聚合与命名规范

把昂贵的查询预先算好存成新 series。这一章讲 recording rules 的价值(查询提速几个数量级 + 降基数)、那个被低估的官方命名规范 level:metric:operations,以及什么该做成规则、什么不该。

作者 David更新于 第 32 篇(共 52 篇)

recording rules · 预聚合与命名规范

规则引擎的另一个用途:recording rule——把一个查询周期性地算好,结果存成一条新的 series。

它是查询性能的利器,但用好它需要一点纪律。

它干什么

groups:
- name: api-aggregations
  interval: 30s
  rules:
  - record: service:http_requests:rate5m
    expr: sum by (service) (rate(http_requests_total[5m]))

每 30 秒,Prometheus 跑一次这个表达式,把结果当成新指标写回 TSDB。之后你查 service:http_requests:rate5m 就是查一条普通 series——几乎零成本。

收益有多大?回到成本模型:

原查询:  sum by (service) (rate(http_requests_total[5m]))
  ① 选中 series:  50,000 条
  ② 每次读点数:   20
  ③ 求值次数:     每个用户、每次刷新面板 × 每个 step

recording rule 之后:
  ① 选中 series:  10 条(每个 service 一条)  ← 5000 倍!
  ② 每次读点数:   1
  ③ 求值次数:     不变,但每次都便宜到忽略不计

而规则本身:每 30 秒算一次(不管有多少人在看)

关键洞见:成本从“每个观众 × 每次刷新”变成“每 30 秒一次”。 一个被 20 个面板引用的查询,原本要算 20×N 次,现在只算 1 次。观众越多,收益越大。

官方命名规范:被低估的三段式

Prometheus 官方为 recording rule 定了一个命名规范,很多人不知道,但它极有价值:

level:metric:operations

三段,冒号分隔:

段含义例子
level聚合到什么层级(即 by 里剩下的标签)service、instance、job
metric原指标名http_requests
operations做了什么操作(倒序)rate5m、sum:rate5m
service:http_requests:rate5m
 └level┘ └── metric ──┘└ops┘
 聚合到 service 层级 · 原指标 http_requests · 做了 rate5m

instance:node_cpu:rate5m
job:http_errors:ratio_rate5m

为什么这个规范重要?因为它让名字自带元信息**:**

  • 一眼知道聚合层级 —— service:xxx 告诉你“这条已经按 service 聚合过了,没有 instance 维度”。你不会误以为还能按实例切。
  • 一眼知道能不能再聚合 —— 看到 service:http_requests:rate5m,你知道它已经 rate 过了,不能再 rate() 一次(那是错的)。
  • 避免和原始指标混淆 —— 冒号是关键:原始指标(来自抓取)永远不含冒号,而 recording rule 的产物必含冒号。看到冒号 = 这是算出来的,不是采出来的。

这条“冒号即派生”的约定,是整个规范里最有价值的一点——它让任何人看到一个指标名,立刻知道它的来路。

什么该做成 recording rule

判据只有一条:这个查询是不是被反复执行?

✅ 该做:

❌ 不该做:

  • 只用一次的查询 —— 规则也有成本(每个 interval 都要算),用一次不值。
  • 探索性查询 —— 你还在试的东西,别固化。
  • 会产生高基数的规则 —— 这是个真实的坑:record 的产物也是 series,也吃基数!如果你的规则 sum by (pod) 而 pod 有 10 万个,你刚刚创造了 10 万条新 series。recording rule 应该降基数,不是造基数。

一个高价值套路:SLO 多窗口预聚合

下一章讲 SLO 燃烧率时会用到多个时间窗口的错误率。它们是 recording rule 的经典用武之地:

groups:
- name: slo
  interval: 30s
  rules:
  # 分子分母各存一份,多个窗口
  - record: service:http_requests:rate5m
    expr: sum by (service) (rate(http_requests_total[5m]))
  - record: service:http_errors:rate5m
    expr: sum by (service) (rate(http_requests_total{status=~"5.."}[5m]))
  - record: service:http_requests:rate1h
    expr: sum by (service) (rate(http_requests_total[1h]))
  - record: service:http_errors:rate1h
    expr: sum by (service) (rate(http_requests_total{status=~"5.."}[1h]))
  # 错误率(依赖上面的,所以必须同组且在后!)
  - record: service:http_errors:ratio_rate5m
    expr: service:http_errors:rate5m / service:http_requests:rate5m
  - record: service:http_errors:ratio_rate1h
    expr: service:http_errors:rate1h / service:http_requests:rate1h

注意最后两条依赖前面的 —— 这就是上一章说的“同 group 内顺序执行”的实际用途。如果把它们放到不同 group,可能读到上一轮的结果或者干脆是空。

也注意为什么分子分母要分开存,而不是直接存比值: 因为比值不可再聚合——存了 service 级的比值,你就没法再算出全局比值了(和 summary 的分位数不可聚合是同一个道理)。分子分母分开存,保留了后续聚合的自由度。 这是个容易忽略但很重要的设计判断。

实战注意事项

① 规则也会失败,要监控它

# 规则求值失败
rate(prometheus_rule_evaluation_failures_total[5m]) > 0
# 规则求值太慢(超过 interval 就麻烦了)
prometheus_rule_group_last_duration_seconds
  > prometheus_rule_group_interval_seconds

规则组求值时间超过它的 interval,规则就开始堆积、延迟——这是个必须告警的条件。

② interval 别比抓取间隔还密

规则算的是已抓回的数据,抓取间隔 15s 的话,规则每 5s 算一次纯属浪费(数据根本没变)。规则 interval ≥ 抓取间隔。

③ 改规则不影响历史数据

recording rule 的产物是从规则生效那一刻开始才有的。改了表达式,历史数据不会重算——你会得到一条在某时刻语义突变的 series。这和改指标名的问题一样:第一次就想清楚。

小结与下一章

recording rule 把查询周期性算好、结果存成新 series——成本从“每个观众 × 每次刷新”变成“每 30 秒一次”,观众越多收益越大。官方命名规范 level:metric:operations 让名字自带元信息:聚合层级、做过什么操作、能不能再聚合;最有价值的是“冒号即派生”——原始指标永不含冒号,看到冒号就知道是算出来的。该做:被多处引用的、昂贵聚合的、子查询内层的、长期趋势要用的;不该做:只用一次的、探索性的、以及会造基数的(规则产物也吃基数,它该降基数不该造基数)。经典套路是 SLO 多窗口预聚合——注意依赖前序规则必须同组且在后,以及分子分母要分开存(比值不可再聚合,和 summary 分位数同理)。实战:监控规则失败与求值超时、interval ≥ 抓取间隔、改规则不重算历史。

规则会写了。但最难的问题还没答:阈值到底该定多少? 下一章。👉 阈值怎么定。

参考资料

  • [1] Prometheus 源码 rules/recording.go、rules/group.go(组内顺序执行)。
  • [2] Prometheus 官方文档:Recording rules(含 level:metric:operations 命名规范)。
  • [3] 关联查询性能、聚合、基数。

本页目录