记录规则在指定评估时刻执行 PromQL 表达式,把结果作为一个新指标保存。反复刷新的面板可以读取这个指标,减少重复计算。但简单把它称为缓存,会遮住一个重要区别:记录序列有自己的样本历史、写入节奏和标签集合,并不是任意查询请求的透明缓存。使用者必须知道读到的是哪次预计算,以及这个结果保留了哪些信息。

三个时钟共同影响页面上的值

抓取决定原始观测何时进入存储,规则评估决定表达式何时生成记录样本,面板则决定何时以及以什么步长查询两者。把页面刷新得更快,不会让记录规则提前计算。因此,在两次规则评估之间读取记录指标,可能得到比同一时刻直接执行原表达式更旧的值。这是不同采样节奏的正常结果,不能仅凭两条曲线不同便断定计算错误。

规则组可以指定周期;未单独指定时,采用全局规则评估周期。同组规则按顺序执行,并使用相同的评估时间戳,所以后面的规则可以使用前面规则刚生成的结果。独立规则组即使写了相同的周期,也不能因此推导存在同样的先后顺序。依赖关系需要明确设计,不能依靠某次运行中碰巧出现的执行顺序。

预计算也改变了保留的数据

聚合表达式决定输出保留哪些维度。如果规则把 instance 求和消掉,面板之后就无法仅从这条记录序列恢复实例分布。仍需实例级排查时,应保留原始指标,并围绕已知消费者选择记录维度。每个剩余标签组合又是一条需要存储的新序列,因此把所有可能的聚合都做成规则,可能显著增加写入量和存储量,并不总是节省资源。

修改表达式会影响后续评估。正常重载规则不会用新定义重新计算全部旧样本,也不会自动填补规则创建之前的历史。在相同指标名下改变含义,可能让同一张图同时包含两套不兼容口径。应记录定义和时间窗口,在消费者要求历史可比时安排明确迁移,不能把正常重载解释为历史数据已经整体更新。

坏掉的样子

面板看起来停住,但原始样本仍在进入数据库时,应对照记录序列最近样本、规则组最近评估、执行耗时与错误。如果上一轮还没结束,下一次计划评估到来时会被跳过,直到当前轮完成或超时。把昂贵查询变成记录规则,只是把计算移入调度器,没有让计算成本消失。执行耗时长期接近周期时,新鲜度会比配置值更差。

查询变快也可能是因为聚合后回答的问题更窄。庆祝速度提升前,应确认被移除的维度确实不再需要。简单表达式通常不值得只为性能增加记录规则。应先测量重复查询成本和复用频率,再把新增序列、周期执行以及新鲜度延迟一起纳入判断。面板数量很多,也不等于每个表达式都必须预先保存。

快速验证

保存为 recording-cadence.py,运行 python3 recording-cadence.py。环境需要已经安装的 promtool,可以从 PATH 找到,或用 PROMTOOL 指定其可执行文件路径。本轮使用 Python 3.12.14 与 promtool 3.14.0。真实查询引擎在临时目录中评估合成样本,不启动服务器、监听端口或容器,也不联系外部服务。

from pathlib import Path
from tempfile import TemporaryDirectory
import os
import shutil
import subprocess

# Uses a real, installed promtool; no server or Docker runtime is started.
promtool = shutil.which(os.environ.get("PROMTOOL", "promtool"))
if not promtool:
    raise SystemExit("Set PROMTOOL to an installed promtool executable")

tests = r"""evaluation_interval: 2m
rule_files:
  - rules.yml
tests:
  - interval: 1m
    input_series:
      - series: "queue_depth{queue=\"orders\"}"
        values: "1 4 9 16"
    promql_expr_test:
      - expr: "sum(queue_depth)"
        eval_time: 1m
        exp_samples:
          - labels: "{}"
            value: 4
      - expr: "queue:depth:sum"
        eval_time: 1m
        exp_samples:
          - labels: "{__name__=\"queue:depth:sum\"}"
            value: 1
      - expr: "queue:depth:sum"
        eval_time: 2m
        exp_samples:
          - labels: "{__name__=\"queue:depth:sum\"}"
            value: 9
      - expr: 'timestamp(queue:depth:sum)'
        eval_time: 1m
        exp_samples:
          - labels: '{}'
            value: 0
      - expr: 'timestamp(queue:depth:sum)'
        eval_time: 2m
        exp_samples:
          - labels: '{}'
            value: 120
      - expr: 'queue:depth:double'
        eval_time: 1m
        exp_samples:
          - labels: '{__name__="queue:depth:double"}'
            value: 2
      - expr: 'queue:depth:double'
        eval_time: 2m
        exp_samples:
          - labels: '{__name__="queue:depth:double"}'
            value: 18
"""
rules = r"""groups:
  - name: slow-recording
    interval: 2m
    rules:
      - record: queue:depth:sum
        expr: sum(queue_depth)
      - record: queue:depth:double
        expr: queue:depth:sum * 2
"""

with TemporaryDirectory(prefix="prometheus-query-check-") as directory:
    root = Path(directory)
    (root / "tests.yml").write_text(tests, encoding="utf-8")
    if rules:
        (root / "rules.yml").write_text(rules, encoding="utf-8")
    result = subprocess.run([promtool, "test", "rules", "tests.yml"],
                            cwd=root, text=True, capture_output=True, timeout=45)
    print(result.stdout, end="")
    if result.returncode:
        print(result.stderr, end="")
        raise SystemExit(result.returncode)

预期输出为 SUCCESS。第一分钟原始求和结果是四,但记录值仍为第零分钟的一;第二分钟记录序列更新为九。这里故意选择很便宜的表达式,以隔离周期差异,不是在建议为每个队列仪表值建立记录规则,也没有测量真实调度器在负载下的性能。输入时间线和断言直接说明为何同一时刻两个查询可以返回不同结果。

执行结束后会删除临时测试文件。实验建立的是评估之间记录值与即时值可能合理不同这一事实。实际部署时,还需要确认规则加载、执行耗时、输出基数和消费查询。仅看到文件重载成功,并不能证明新指标已经积累了长时间面板所需的历史,也不能证明后续评估没有跳过或失败。

额外检查读取记录样本的时间戳:第一分钟仍是零,第二分钟变成一百二十秒。同组后续规则把前一条结果乘二,在这两个查询时刻分别得到二和十八。这些检查区分响应刚返回与存储观测刚更新,并实际覆盖同组依赖顺序。它们没有创建跨组调度保证,也没有模拟服务在负载下错过评估的行为。

来源