recording rules · 预聚合与命名规范
把昂贵的查询预先算好存成新 series。这一章讲 recording rules 的价值(查询提速几个数量级 + 降基数)、那个被低估的官方命名规范 level:metric:operations,以及什么该做成规则、什么不该。
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
判据只有一条:这个查询是不是被反复执行?
✅ 该做:
- 被多个面板 / 告警引用的查询 —— 一处算好,处处受益。
- 昂贵的聚合 —— 从几万条 series 聚合到几条。
- 子查询的内层 —— 把
max_over_time(rate(x[5m])[1h:1m])的内层rate预算好,成本降几个数量级。 - 长期趋势要用的数据 —— 比如“每天的 P99”,预聚合后可以长期保存而不必留全部原始数据。
❌ 不该做:
- 只用一次的查询 —— 规则也有成本(每个 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 ≥ 抓取间隔、改规则不重算历史。
规则会写了。但最难的问题还没答:阈值到底该定多少? 下一章。👉 阈值怎么定。