当查询成本和复用频率足以支持预计算时,反复使用的服务级查询适合做成记录规则。关键问题是保存什么。如果只保存每个实例的错误百分比,可能丢掉之后聚合必需的流量规模;同时保存错误速率与请求速率,则保留了分子和分母,消费者能够据此计算按实际请求量加权的服务错误占比,而不是把每台实例当成相同流量。

百分比必须对应明确的总体

假设繁忙实例每秒处理十个请求,每秒发生零点一个错误;空闲实例每秒处理一个请求,却每秒发生零点五个错误。两个实例的错误占比分别为百分之一与百分之五十。直接平均得到百分之二十五点五,相当于给两个实例相同权重,但它们的请求量实际上相差十倍。这个数描述平均实例百分比,不描述服务处理的全部请求。

服务合计每秒处理十一个请求,发生零点六个错误,因此错误占比是 0.6/11,约百分之五点四五。这是聚合错误量除以聚合请求量。两侧必须描述同一组符合条件的流量,并使用相同时间窗口。如果分子排除了分母包含的一类失败,或者两侧聚合了不同服务,面板即使计算得很快,结果也仍然不成立。

按顺序建立规则依赖

先对每条计数器序列应用 rate,使已经观测到的重置能够在聚合前得到处理,再按照消费者确实需要的维度求和。本例依次记录服务与实例维度的请求速率、服务请求速率、服务错误速率,以及最终服务错误占比。第一个实例级中间结果用于展示明确的前后依赖,后续规则实际消费这个结果,而不是重复写一个看似相关的名字。

四条规则位于同一个组,按声明顺序运行,使用相同评估时间。分子与分母都保留匹配的 service 标签,因此除法能够把对应服务配对。如果分母额外保留实例标签,就会改变向量匹配关系,可能导致没有结果,或需要重新设计连接方式。测试应同时检查输出标签和数值,不能只看到一个合理百分比就认为查询正确。

命名应说明聚合层级和范围。两分钟速率与三十分钟速率即使当前值恰好相同,也不是同一种量。应在规则旁记录哪些事件计入总体,并明确消费它的面板与告警。沿用旧名称但修改公式,只会改变未来记录样本,不会重新解释已经写入的历史。需要保持时间可比性时,定义变更必须被消费者看见。

坏掉的样子

平均百分比可能让一台低流量故障实例主导整个服务报告,也可能掩盖高流量实例的失败。提高评估频率不会修复这个算术问题,应恢复底层计数或速率,在分别聚合后再相除。如果最后只保留了百分比,通常无法仅靠这些百分比重建原始流量权重,因为相同占比可以来自完全不同的请求数量。

零流量是另一类情况。分母为零时,错误占比并没有被定义为零;分子缺失也可能代表仪表化缺少序列,而不是没有错误。应根据指标契约明确这些语义,避免自动把所有空结果或未定义数值显示成健康的零。还应确认健康期间错误计数器是否持续导出,避免只有发生错误后才创建序列,导致正常状态与数据缺失难以区分。

快速验证

保存为 recording-weighted.py,在 PATH 中准备已安装的 promtool,或用 PROMTOOL 指定可执行文件,再运行 python3 recording-weighted.py。完整程序在 promtool 3.14.0 与 Python 3.12.14 上通过。真实规则引擎评估每分钟一份的合成计数器,包含权重明显不同的实例,以及独立的零流量、序列缺失案例。

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

promtool = shutil.which(os.environ.get("PROMTOOL", "promtool"))
if promtool is None:
    raise SystemExit("Set PROMTOOL to an installed promtool executable")
promtool = str(Path(promtool).resolve())
tests = r"""evaluation_interval: 1m
rule_files:
  - rules.yml
tests:
  - interval: 1m
    input_series:
      - series: "http_requests_total{service=\"checkout\",instance=\"busy\"}"
        values: "0+600x5"
      - series: "http_errors_total{service=\"checkout\",instance=\"busy\"}"
        values: "0+6x5"
      - series: "http_requests_total{service=\"checkout\",instance=\"quiet\"}"
        values: "0+60x5"
      - series: "http_errors_total{service=\"checkout\",instance=\"quiet\"}"
        values: "0+30x5"
    promql_expr_test:
      - expr: "service:http_requests:rate2m"
        eval_time: 5m
        exp_samples:
          - labels: "{__name__=\"service:http_requests:rate2m\",service=\"checkout\"}"
            value: 11
      - expr: "abs(service:http_errors:rate2m - 0.6) < bool 1e-12"
        eval_time: 5m
        exp_samples:
          - labels: "{service=\"checkout\"}"
            value: 1
      - expr: "abs(service:http_errors:ratio_rate2m - 0.6/11) < bool 1e-12"
        eval_time: 5m
        exp_samples:
          - labels: "{service=\"checkout\"}"
            value: 1
      - expr: "abs(avg(rate(http_errors_total[2m]) / rate(http_requests_total[2m])) - 0.255) < bool 1e-12"
        eval_time: 5m
        exp_samples:
          - labels: "{}"
            value: 1
  - interval: 1m
    input_series:
      - series: 'http_requests_total{service="idle",instance="one"}'
        values: "0+0x5"
      - series: 'http_errors_total{service="idle",instance="one"}'
        values: "0+0x5"
    promql_expr_test:
      - expr: 'count(service:http_errors:ratio_rate2m != service:http_errors:ratio_rate2m)'
        eval_time: 5m
        exp_samples:
          - labels: "{}"
            value: 1
  - interval: 1m
    input_series: []
    promql_expr_test:
      - expr: 'count(service:http_errors:ratio_rate2m) or vector(0)'
        eval_time: 5m
        exp_samples:
          - labels: "{}"
            value: 0
"""
rules = r"""groups:
  - name: service-rates
    interval: 1m
    rules:
      - record: instance:http_requests:rate2m
        expr: sum by (service, instance) (rate(http_requests_total[2m]))
      - record: service:http_requests:rate2m
        expr: sum by (service) (instance:http_requests:rate2m)
      - record: service:http_errors:rate2m
        expr: sum by (service) (rate(http_errors_total[2m]))
      - record: service:http_errors:ratio_rate2m
        expr: service:http_errors:rate2m / service:http_requests:rate2m
"""
with TemporaryDirectory(prefix="prom-rule-test-") as directory:
    work = Path(directory)
    (work / "tests.yml").write_text(tests, encoding="utf-8")
    (work / "rules.yml").write_text(rules, encoding="utf-8")
    subprocess.run([promtool, "test", "rules", "tests.yml"], cwd=work,
                   check=True, timeout=45)

预期输出为 SUCCESS。记录请求速率为十一,错误速率为零点六,最终占比为 0.6/11。另外一项断言明确展示,错误的实例比例平均法得到 0.255。涉及浮点除法的比较使用很小的数值容差,标签断言则确认仍保留 checkout 服务身份。这些检查同时覆盖计算次序、依赖输出和最终聚合口径。

零流量案例得到 NaN,它与自身比较不相等,因此过滤比较保留一条序列;输入缺失时则没有比例序列,两者都不应被当成健康的零。这个本地规则测试不启动监听端,临时文件会清理。它不是性能基准,也不能证明生产仪表化完整。部署前还应补充符合服务实际的重置和入库延迟案例,并测量查询成本及规则耗时,再判断是否有运行收益。

来源