成本可观测、SLO 与告警
成本首页连续几天显示“本周费用下降”,平台团队据此完成了节省汇报。直到财务对账才发现,账单导出任务早已停止,Kubernetes 分摊采集也因为凭证过期只剩一半集群。仪表盘服务仍返回 200,最后一次成功数据还完整保存在数据库里,所有图表都能正常渲染。系统可用,成本事实却已经陈旧。
另一次事故中,采集任务每天都成功,金额总和也与云账单一致,但大量资源失去 owner 后被静默塞进“共享成本”。总额没有异常,部门费用却严重失真。成本平台的可靠性不能只看进程存活和 HTTP 状态;它必须证明数据按期到达、范围完整、分摊可解释、金额守恒,并且异常能在关账或采购决策前到达真正有处置权的人。
成本可靠性有四个观察面
运行可用性回答服务能否请求;数据新鲜度回答最新完整批次是否到达;分摊完整性回答费用能否归到稳定 owner;财务一致性回答成本结果能否与源账单守恒。四者必须分开建模:
runtime availability
!= delivery freshness
!= allocation coverage
!= financial correctness成本数据还具有天然延迟。云账单会晚到、修订或补发,Kubernetes 成本依赖指标窗口,合同与汇率有独立生效口径。告警不能把“供应商尚未交付”误判成内部任务失败,也不能用“账单最终会到”掩盖已超过业务关账窗口的延迟。
每个数据源都保存一份交付契约:
sourceId: cloud-billing-primary
owner: finops-data
expectedCadence: daily
allowedDeliveryLag: 36h
completenessKey: [billingAccount, chargePeriod, deliveryBatch]
amountBasis: EffectiveCost
currencyBasis: BillingCurrency
allocationTarget: 0.98
reconciliationTolerance: 0.000001
consumers:
- showback
- budget-guardrail
- commitment-review
escalation:
warning: finops-data
critical: finance-closeallowedDeliveryLag 要来自供应商交付特性和内部决策窗口,而不是统一写成五分钟。allocationTarget 允许少量可解释异常,但未分配金额必须进入异常队列,不能为达到 SLO 把它改名为共享成本。
先定义能计算的 SLI
数据新鲜度以最新完整批次的业务水位计算,不使用任务最后心跳:
freshness_seconds = observation_time - latest_complete_charge_time如果任务不断重试并刷新心跳,心跳看起来很新,账单水位仍然陈旧。完整批次需要通过 schema、分区数量、行数、金额和去重门禁后才能推进 latest_complete_charge_time。
分摊覆盖率同时保留金额和记录两个维度:
allocation_amount_coverage = allocated_amount / allocatable_amount
allocation_row_coverage = allocated_charge_rows / allocatable_charge_rows只看金额会让大量小额资源失去 owner,只看行数又可能漏掉一笔大额共享服务。未分配、身份冲突和待争议费用分别计数,不能都归入 unknown。
处理延迟描述同一批次从落地到可消费的时间:
allocation_lag_seconds = allocation_published_time - source_delivery_time对账差异必须按可审计边界计算:
reconciliation_delta = source_effective_cost - allocated_effective_cost全局差异为零仍可能由一笔多分和另一笔少分互相抵消,因此还要记录 reconciliation_failed_charge_count。预算、单位经济、承诺采购等下游只消费通过门禁的批次。
把数据产品状态暴露成低基数指标
沿用现有 Prometheus 与 Grafana 平台,不为成本文章再部署一套监控系统。平台安装、存储、远端写入和通用告警治理见 Prometheus 与 Grafana;跨服务埋点、Collector 和遥测处理见 OpenTelemetry。成本域只新增一个受控 exporter 或批处理指标出口。
最小指标集合可以是:
finops_source_latest_complete_timestamp_seconds{source="cloud-billing"} 0
finops_source_delivery_lag_seconds{source="cloud-billing"} 7200
finops_allocation_amount_coverage_ratio{source="cloud-billing"} 0.992
finops_allocation_row_coverage_ratio{source="cloud-billing"} 0.981
finops_allocation_lag_seconds{pipeline="daily-allocation"} 1800
finops_reconciliation_delta{source="cloud-billing",currency="USD"} 0
finops_reconciliation_failed_charges{source="cloud-billing"} 0
finops_unallocated_amount{source="cloud-billing",currency="USD"} 120
finops_batch_state{pipeline="daily-allocation",state="published"} 1示例中的 0 表示测试时间原点,生产 exporter 写入最新完整批次的 Unix 秒。正式规则测试使用 T0 相对时间。不要把 account ID、resource ID、namespace、项目名、合同、标签值或 owner 邮箱放进指标 label;这些高基数且敏感的维度留在受控证据仓库,通过 batch_id 或匿名 evidence key 关联。
Exporter 只读取已经聚合的状态表:
CREATE VIEW finops_observability_status AS
SELECT
source_id,
MAX(CASE WHEN batch_status = 'complete' THEN charge_window_end END) AS latest_complete_charge_time,
MAX(delivery_time) AS latest_delivery_time,
SUM(allocated_amount) / NULLIF(SUM(allocatable_amount), 0) AS amount_coverage,
SUM(allocated_rows) / NULLIF(SUM(allocatable_rows), 0) AS row_coverage,
SUM(source_amount - allocated_amount) AS reconciliation_delta,
SUM(failed_charge_count) AS failed_charge_count
FROM finops_batch_evidence
GROUP BY source_id;不允许 exporter 直接扫描原始账单明细。指标查询应在受控聚合表上运行,避免 Prometheus scrape 把数据仓库拖垮,也避免监控身份获得合同明细读取权限。
用记录规则统一 SLI 口径
保存为 finops-sli.rules.yml:
groups:
- name: finops-sli
interval: 5m
rules:
- record: finops:source_freshness_seconds
expr: time() - finops_source_latest_complete_timestamp_seconds
- record: finops:allocation_coverage_ratio
expr: >-
min by (source) (
finops_allocation_amount_coverage_ratio,
finops_allocation_row_coverage_ratio
)
- record: finops:reconciliation_is_clean
expr: >-
(abs(finops_reconciliation_delta) <= bool 0.000001)
* on (source) group_left
(finops_reconciliation_failed_charges == bool 0)取金额与行覆盖率的较小值,是为了让任一维度退化都可见。bool 比较把两项条件稳定输出成 0 或 1,再通过向量匹配相乘;否则失败时序会被过滤,后续 == 0 告警反而没有输入。reconciliation_is_clean 同时要求金额容差和逐费用失败数为零;如果多币种数据尚未转换到同一批准口径,应按 source + currency 分开判断,不能直接求和。
上线前运行:
promtool check rules finops-sli.rules.yml预期输出包含 SUCCESS。语法检查只证明规则可解析,不证明指标含义正确,下一步必须做规则单元测试。
正向实验:完整批次保持安静
保存告警规则 finops-alerts.rules.yml:
groups:
- name: finops-alerts
rules:
- alert: FinOpsSourceStale
expr: finops:source_freshness_seconds > 129600
for: 30m
labels:
severity: critical
domain: finops
annotations:
summary: "成本源完整水位超出交付窗口"
runbook: "https://<internal-runbook>/finops/source-stale"
- alert: FinOpsAllocationCoverageLow
expr: finops:allocation_coverage_ratio < 0.98
for: 1h
labels:
severity: warning
domain: finops
annotations:
summary: "成本分摊覆盖低于批准阈值"
- alert: FinOpsReconciliationFailed
expr: finops:reconciliation_is_clean == 0
for: 15m
labels:
severity: critical
domain: finops
annotations:
summary: "源账单与分摊结果不守恒"再保存 finops-alerts.test.yml:
rule_files:
- finops-sli.rules.yml
- finops-alerts.rules.yml
evaluation_interval: 5m
tests:
- interval: 5m
input_series:
- series: 'finops_source_latest_complete_timestamp_seconds{source="cloud-billing"}'
values: '0+300x30'
- series: 'finops_allocation_amount_coverage_ratio{source="cloud-billing"}'
values: '0.995x30'
- series: 'finops_allocation_row_coverage_ratio{source="cloud-billing"}'
values: '0.990x30'
- series: 'finops_reconciliation_delta{source="cloud-billing",currency="USD"}'
values: '0x30'
- series: 'finops_reconciliation_failed_charges{source="cloud-billing"}'
values: '0x30'
alert_rule_test:
- eval_time: 2h
alertname: FinOpsAllocationCoverageLow
exp_alerts: []
- eval_time: 2h
alertname: FinOpsReconciliationFailed
exp_alerts: []promtool test rules finops-alerts.test.yml预期输出 SUCCESS,说明覆盖率与对账满足条件时不会误报。这个实验验证规则行为,不代表真实云账单已经按时交付。
反向实验:心跳正常也必须发现陈旧数据
在测试文件追加一个场景,让 exporter 心跳继续增长,但完整账单水位固定,覆盖率同时下降:
- interval: 5m
input_series:
- series: 'finops_source_latest_complete_timestamp_seconds{source="cloud-billing"}'
values: '0x500'
- series: 'finops_exporter_heartbeat_timestamp_seconds{source="cloud-billing"}'
values: '0+300x500'
- series: 'finops_allocation_amount_coverage_ratio{source="cloud-billing"}'
values: '0.94x500'
- series: 'finops_allocation_row_coverage_ratio{source="cloud-billing"}'
values: '0.91x500'
- series: 'finops_reconciliation_delta{source="cloud-billing",currency="USD"}'
values: '12x500'
- series: 'finops_reconciliation_failed_charges{source="cloud-billing"}'
values: '3x500'
alert_rule_test:
- eval_time: 38h
alertname: FinOpsSourceStale
exp_alerts:
- exp_labels:
alertname: FinOpsSourceStale
domain: finops
severity: critical
source: cloud-billing
exp_annotations:
summary: "成本源完整水位超出交付窗口"
runbook: "https://<internal-runbook>/finops/source-stale"
- eval_time: 2h
alertname: FinOpsAllocationCoverageLow
exp_alerts:
- exp_labels:
alertname: FinOpsAllocationCoverageLow
domain: finops
severity: warning
source: cloud-billing
exp_annotations:
summary: "成本分摊覆盖低于批准阈值"
- eval_time: 2h
alertname: FinOpsReconciliationFailed
exp_alerts:
- exp_labels:
alertname: FinOpsReconciliationFailed
currency: USD
domain: finops
severity: critical
source: cloud-billing
exp_annotations:
summary: "源账单与分摊结果不守恒"预期规则测试仍返回 SUCCESS,但含义与正向实验相反:测试成功证明告警按预期触发。若 FinOpsSourceStale 没有触发,优先检查规则是否错误使用 exporter 心跳;若覆盖告警缺少 source,检查聚合是否把不同来源合并。
对账告警还应触发并指向三条失败费用。告警正文不携带费用明细,只携带 source、批次和受控证据链接。值班人员通过授权查询查看差异行,避免敏感金额进入聊天群和工单通知。
SLO 要绑定消费决策
成本 SLO 不是为了做一张绿色面板,而是保护下游决策。可以按消费场景制定不同目标:
| 消费场景 | 关键 SLI | 失败动作 |
|---|---|---|
| 日常 showback | freshness、allocation coverage | 标记 provisional,延迟发布 |
| 预算护栏 | freshness、forecast input coverage | 冻结自动动作,转人工复核 |
| 承诺采购 | 完整周期、费率版本、reconciliation | 禁止生成正式采购建议 |
| 财务关账 | 守恒、争议、币种与批次冻结 | 阻止 chargeback 入账 |
| 节省验收 | baseline 完整性、SLO 关联 | 不确认 realized savings |
SLO 目标要写成“在批准交付窗口内,完整批次满足覆盖和守恒门禁的比例”,而不是笼统的 99.9% 服务可用性。错误预算也以批次或决策窗口消耗:一个影响关账的大批次失败,不能被数千次成功 scrape 稀释。
多窗口告警适合发现持续退化与快速故障。新鲜度超过硬截止窗口立即升级;覆盖率轻微下降可等待一个分摊周期,避免标签短暂同步造成噪声;逐费用不守恒通常直接阻断发布。告警阈值、for 时长和严重级别必须对应可执行动作。
路由与排障从批次证据开始
每条告警至少携带 source、pipeline、batch_id、数据水位、失败门禁、影响消费者、owner 和 runbook。不要携带真实账号、资源 ID、合同折扣或租户名称。路由按故障责任拆分:
数据未交付:账单集成 owner 检查对象存储、导出配置、凭证和供应商状态。分摊覆盖下降:资产身份 owner 检查标签、历史映射和删除资源快照。处理延迟升高:成本平台 owner 检查队列、仓库、锁、容量和失败重试。
对账不守恒:FinOps 数据 owner 冻结批次,定位重复、缺失、修订和舍入。关账窗口受影响:财务 owner 决定延期、例外或使用上一冻结批次。
排障顺序保持分层:先确认供应商应交付的窗口,再确认原始对象和校验和,再确认导入批次与 schema,随后检查身份和分摊 trace,最后检查聚合与仪表盘。直接重跑整个流水线可能覆盖首次失败证据,还可能重复生成 chargeback 结果。
常见现象和判断:
| 现象 | 判断证据 | 可能原因 | 修复后再验证 |
|---|---|---|---|
| exporter up,freshness 告警 | 完整水位固定、心跳增长 | 导入空跑或完整性门禁失败 | 新批次通过且水位推进 |
| coverage 降,金额守恒 | unallocated 与 identity conflict 增长 | owner 映射或标签失效 | 覆盖恢复且异常有 owner |
| delta 为零,failed charge 非零 | 逐费用差异相互抵消 | 重复与缺失并存 | 失败费用数归零 |
| allocation lag 增长 | 队列深度、批次耗时、仓库锁 | 容量不足或重算抢占 | 延迟回到预算且无丢批 |
| 告警风暴 | label 基数、每资源规则 | 把资源身份放进时序 | 聚合到 source/pipeline |
权限和敏感信息决定观测边界
Exporter 身份只读聚合状态视图,Prometheus 只抓取低基数指标,Grafana 查询指标或授权 API,Alertmanager 只接收匿名维度。原始账单读取、合同费率、跨账户映射和资源明细留在成本数据域。任何告警平台都不应因为排障方便获得完整账单读取权限。
运行指标可能泄露业务规模、云厂商使用、成本趋势和组织结构。对 Grafana folder、Prometheus query、远端写入、告警历史和通知渠道实施 RBAC;外发通知隐藏金额,只显示比例、状态和 evidence key。下载明细需要短期授权与审计,导出对象设置 TTL。
凭证分成账单采集、状态查询、指标暴露、监控抓取和通知发送五类,分别轮换。轮换实验要证明新凭证生效、旧凭证失效、数据水位继续推进;只看到 exporter 进程存活不算成功。
容量成本来自批次数和基数
成本指标的危险不是采样频率,而是维度爆炸。account × resource × namespace × owner × tag × currency 会迅速制造数百万时序,并把敏感身份复制到监控存储。指标只保留 source、pipeline、currency 和少量状态;资源级明细通过日志或数据仓库查询。
容量估算包括数据源数、批次频率、规则数量、保留时间、远端写入副本、告警评估间隔和重算峰值。账单补发时多个历史分区可能同时重算,exporter 查询应读取预聚合状态,不能在 scrape 时执行全表扫描。对 exporter 设置查询超时和陈旧缓存标记,失败时暴露 finops_exporter_last_success_timestamp_seconds,但不拿它替代业务水位。
观测本身也有费用。仪表盘查询、长保留、高基数标签、跨区域远端写入和通知平台都应进入共享成本池。减少保留前先确认关账、审计和事故复盘是否依赖这些时序;长期财务证据应保存在受控账本,不依赖 Prometheus 作为唯一记录系统。
变更、回滚和退出保留证据连续性
规则升级先执行 promtool check rules 和正反单元测试,再在影子规则组中比较告警数量、持续时间和影响批次。修改阈值时记录原因、历史回放结果、审批人与生效窗口。直接降低阈值来消除告警,会让 SLO 失去决策保护作用。
上线后出现误报,先静默具体 source 并保留原始事件,再回滚规则文件;不要全局关闭成本告警。出现漏报则冻结受影响的预算动作、采购建议和关账批次,补算失败窗口并生成事件记录。规则回滚不能回滚已经推进的数据水位,必须明确哪些批次需要重评估。
清理测试环境时删除测试规则、临时 ServiceMonitor、测试指标、Grafana dashboard、通知路由和短期凭证,确认 Prometheus target 消失、Alertmanager 无遗留 silence、对象存储无测试账单。生产退出某个成本平台时,先让替代平台并行产生相同 SLI,对比水位、覆盖、延迟和逐费用守恒;切换后撤销旧 exporter 身份、停止 scrape、删除告警路由并归档规则与历史事件。
一套成熟的成本可观测体系,最终能在任何绿色图表之前回答:最新完整数据是什么批次,哪些费用尚未归属,金额是否逐条守恒,哪些决策因此被阻断,以及谁能恢复它。只有这些问题有稳定答案,成本 SLO 才真正保护了预算、采购与财务责任。
