FOCUS 多云成本规范化
同一个平台同时使用 AWS、Google Cloud 和 Azure 后,财务提出了一个看似简单的问题:按服务、区域和业务 owner 比较三家云的有效成本。工程团队把三份导出表做了 UNION ALL,很快就遇到一串互相矛盾的结果:一家的 cost 是未摊销价格,另一家已经扣除 credit,第三家的资源标识藏在扩展列里;月底修正数据到达后,同一张报表又无声改变。
FOCUS 解决的是成本与用量数据的共同语义,不是把供应商账单压成最小公分母。它让 BillingAccountId、ServiceName、ResourceId、BilledCost、EffectiveCost、ChargePeriodStart 等概念可以跨数据生成方理解,同时要求消费端保留交付方式、修正风格、规范版本和供应商扩展。真正可用的多云账本既有统一查询面,也有回到原始费用行的退路。
先分清规范版本与供应商交付版本
FOCUS 规范当前发布线为 1.4。1.3 引入数据生成方计算的 Split Cost Allocation,1.4 进一步定义 Correction Handling、Delivery Handling、Dataset Completeness 和 Dataset Configuration,并补强账期、发票与承诺数据集治理。规范的新能力不会自动出现在云厂商已经交付的数据集中。
工程上必须同时保存三个版本:
spec_version = FOCUS 规范版本
generator_schema = 云厂商实际导出版本
adapter_version = 企业映射与质量规则版本AWS Data Exports 当前可交付 FOCUS 1.2 with AWS columns;Google Cloud FOCUS usage cost export 仍是 Preview,采用清单标识为 1.2;Azure Cost Management 的 FOCUS schema 标识为 1.2-preview,并保留 x_ 扩展列。消费端不能因为目标模型按 1.4 设计,就强制源数据出现 1.3 才引入的 AllocatedResource* 字段。
第一次接入前分别查看 FOCUS 1.4 规范、AWS Data Exports 类型、Google Cloud FOCUS export 与 Azure FOCUS schema,把实际导出版本写入数据合同,而不是写进报表 SQL。
三层账本避免丢失供应商事实
一个可退出、可升级的实现至少分为三层:
provider_billing_raw 保存供应商文件或表、delivery scope、对象路径、批次摘要、schema 指纹和全部原生列。它不能被最新批次直接覆盖,也不能先删掉“看起来无用”的扩展列。
focus_normalized 只承载已有证据支持的 FOCUS 字段。供应商字段没有标准等价物时进入命名空间化扩展,例如 aws_extensions、gcp_extensions、azure_extensions,而不是硬塞进相近标准列。
allocation/showback 承载企业 owner、成本中心、共享池和 Kubernetes 工作负载等派生语义。它可以消费 FOCUS,但不能反向改写原始账单;每条派生记录必须保留 source_charge_key 和 adapter_version。
建立数据合同而不是靠列名猜测
最小合同建议包含:
datasetContract:
provider: aws
dataset: focus-cost-and-usage
specVersion: "1.2"
generatorSchema: "focus-1.2-with-aws-columns"
adapterVersion: "focus-adapter-v3"
correctionStyle: replacement
deliveryMechanism: overwrite
currencyPolicy: source-currency-first
requiredFields:
- BillingAccountId
- ChargePeriodStart
- ChargePeriodEnd
- ServiceProviderName
- ServiceName
- ChargeCategory
- BilledCost
- EffectiveCost
extensionNamespace: aws_extensions
lineageFields:
- delivery_id
- source_object
- source_row_hashrequiredFields 不是把所有 1.4 列设为非空。FOCUS 对不同 Charge Category、数据集和供应商能力有条件约束;门禁应依据源版本和行类型判断。EffectiveCost 适合权责发生口径的趋势和分摊,BilledCost 适合发票口径,二者不能混加或互相填充。
统一事实表可用下面的骨架:
CREATE TABLE focus_normalized (
provider VARCHAR NOT NULL,
billing_account_id VARCHAR NOT NULL,
charge_period_start TIMESTAMP NOT NULL,
charge_period_end TIMESTAMP NOT NULL,
service_provider_name VARCHAR NOT NULL,
service_name VARCHAR,
resource_id VARCHAR,
charge_category VARCHAR NOT NULL,
billing_currency VARCHAR NOT NULL,
billed_cost DECIMAL(38, 12),
effective_cost DECIMAL(38, 12),
tags_json VARCHAR,
provider_extensions VARCHAR,
spec_version VARCHAR NOT NULL,
generator_schema VARCHAR NOT NULL,
adapter_version VARCHAR NOT NULL,
delivery_id VARCHAR NOT NULL,
source_row_hash VARCHAR NOT NULL
);金额使用十进制定点数,不用浮点数。时间保留 UTC 原值和供应商账期语义,报告时区在消费层转换。标签先保留原始键值,再映射稳定 owner;不要把同名标签默认视为同一组织维度。
从一份最小数据跑通转换
不需要先接真实付款账号。用脱敏样例建立 raw-cost.jsonl:
{"provider":"aws","account":"acct-a","service":"Compute","resource":"vm-a","currency":"USD","billed":"12.00","effective":"9.50","chargeStart":"T0","chargeEnd":"T+1h","delivery":"batch-a"}
{"provider":"gcp","account":"acct-b","service":"Compute","resource":"vm-b","currency":"USD","billed":"8.00","effective":"7.20","chargeStart":"T0","chargeEnd":"T+1h","delivery":"batch-b"}下面的 Python 只用于验证合同与守恒,不替代生产 ETL:
import json
from decimal import Decimal
from pathlib import Path
required = {"provider", "account", "currency", "billed", "effective", "delivery"}
totals = {}
for number, line in enumerate(Path("raw-cost.jsonl").read_text(encoding="utf-8").splitlines(), 1):
row = json.loads(line)
missing = required - row.keys()
if missing:
raise ValueError(f"row={number} missing={sorted(missing)}")
billed = Decimal(row["billed"])
effective = Decimal(row["effective"])
key = (row["provider"], row["currency"])
totals.setdefault(key, {"billed": Decimal(0), "effective": Decimal(0)})
totals[key]["billed"] += billed
totals[key]["effective"] += effective
print(json.dumps({
"provider": row["provider"],
"BillingAccountId": row["account"],
"ServiceName": row.get("service"),
"ResourceId": row.get("resource"),
"BillingCurrency": row["currency"],
"BilledCost": str(billed),
"EffectiveCost": str(effective),
"delivery_id": row["delivery"],
"adapter_version": "focus-adapter-v3"
}, ensure_ascii=False))
print({str(k): {m: str(v) for m, v in amounts.items()} for k, amounts in totals.items()})执行:
python normalize_focus.py > normalized.jsonl预期得到两条规范化记录和按 provider + currency 分组的 billed/effective 合计。验收要比较源层与目标层在同一供应商、币种、charge window 下的行数、金额、空值和扩展列覆盖;不能把跨币种总额相等当成守恒。
反向实验:把缺失字段补成零会制造假结论
向样例加入一条只有 billed、没有 effective 的费用,然后把脚本中的严格校验改成:
effective = Decimal(row.get("effective", "0"))脚本会继续运行,但报表会把“供应商未交付或映射失败”解释成“有效成本为零”。这会同时污染节省率、单位经济和分摊守恒。正确做法是保留三种状态:
value present → 使用原值
not applicable → NULL + documented reason
missing/invalid → quarantine + quality incident再做一个版本反例:给 AWS FOCUS 1.2 样例强制要求 AllocatedResourceId。预期不是生成空字符串,而是合同校验明确返回 field_not_delivered_by_generator_schema。Kubernetes 分摊可以从 CUR Split Cost Allocation 或 OpenCost 派生,但必须作为另一条有来源的链,不能伪装成供应商 FOCUS 1.2 原生字段。
修正与交付决定怎样去重
FOCUS 1.4 定义三种修正风格:Replacement 用最新完整快照替换同一 delivery scope;Delta 追加净变化;Ledger 追加原记录冲销与更正重记。Delivery 还区分 Overwrite 与 Append。映射器必须按数据生成方声明处理,不能对所有文件统一 INSERT APPEND。
| 修正风格 | 消费动作 | 常见错误 |
|---|---|---|
| Replacement | 同一 scope 只发布最新完整快照,旧批次保留审计但不参与现值 | 新旧快照同时求和导致翻倍 |
| Delta | 所有增量按相同聚合键累加 | 只读最新文件导致遗漏历史 |
| Ledger | 原行、冲销行和重记行共同构成余额 | 去掉负数行导致成本虚高 |
每次导入保存 delivery_id、delivery_scope、object_checksum、received_at_relative_window 和 correction_style。发布视图前验证同一批次不会重复处理,同一 scope 的 Replacement 只有一个 active 版本,Delta/Ledger 的符号与币种合法。
分摊字段需要守恒和血缘
当数据生成方真正交付 FOCUS 1.3+ 分摊字段时,AllocatedMethodDetails 中的 AllocatedRatio 描述分配比例,已分配行必须能回到 origin charge。每个 origin charge 的比例之和应为 1;未分配部分保持显式,不得静默消失。
SELECT
source_charge_key,
SUM(allocated_ratio) AS ratio_sum,
SUM(effective_cost) AS allocated_effective_cost
FROM focus_allocated
GROUP BY source_charge_key
HAVING ABS(SUM(allocated_ratio) - 1) > 0.000001;预期返回 0 行。若供应商尚未交付这些字段,企业派生层使用独立的 allocation_method_id、allocation_driver_version 和 source lineage,不冒充 FOCUS 原生分摊。
三家云的差异要留在适配层
AWS 的 FOCUS with AWS columns、Google 的 linked dataset 与 Azure 的 x_ 列都有自身交付、权限和保留边界。规范化层应统一可统一的语义,同时把以下差异显式保存:
供应商导出的实际 schema 与 Preview/GA 状态。credit、tax、commitment、marketplace 和 support charge 的原生分类。Resource ID、账号层级、标签格式与组织结构。
迟到数据、账单修正、覆盖或追加行为。Kubernetes workload 分摊来自供应商账单、OpenCost 还是企业规则。
适配器升级先读取一段代表性 raw 批次,生成新旧两个 normalized 表,比较字段覆盖、金额、NULL 分布、扩展列、唯一键和下游 owner 变化。差异没有归因前不切换消费者。
常见故障按证据层定位
| 现象 | 第一证据 | 根因方向 | 修复 |
|---|---|---|---|
| 多云总额比发票少 | provider/raw 与 invoice | 税、支持费、marketplace 或扩展列被裁剪 | 恢复原生列,补映射或保留显式 excluded |
| 同一账期金额翻倍 | delivery scope 与 checksum | Replacement 被当成 Append | 重建 active snapshot,保留旧批次审计 |
| 节省率异常升高 | billed/effective NULL 分布 | 缺失值被补零或 credit 重复扣除 | 隔离坏行,重算 adapter |
| 资源 owner 大量为空 | ResourceId、Tags 与映射版本 | 短生命周期资源或标签传播滞后 | 用时间窗与资产历史补充,保留 unknown |
| Kubernetes 成本消失 | generator schema | 把 FOCUS 1.2 当成已有 allocation 字段 | 接入独立分摊链并保存来源 |
| 升级后历史报表变化 | adapter version 与发布视图 | 原地覆盖映射逻辑 | 双轨产出,按窗口切换并保留旧版本 |
权限与敏感数据边界
原始账单包含付款账号、subscription、resource group、资源 ID、真实折扣、合同、标签和组织结构,属于财务敏感数据。采集身份只读供应商 export 并写 raw 域;适配器读取 raw、写 normalized;应用团队只读按 owner 授权的派生视图。任何单一身份都不应同时修改 export、映射规则和发布报表。
对象存储、BigQuery、Azure Storage、KMS/CMEK 和数仓行列权限需要一起审计。海报、工单和样例只使用 acct-a、resource-a 等占位符,不复制真实账单行。下载和批量导出记录审计事件,临时访问使用短期身份并在任务结束后核销。
容量、成本与长期维护
FOCUS 不会自动降低数据成本。保留原始列、批次、修正和扩展会增加存储;高基数 tags、短生命周期资源和小时级明细会放大查询扫描。容量模型至少包含:
每日源行 × 平均列宽 × 修正放大系数 × 保留窗口
+ normalized 行 × adapter 版本数
+ allocation 行 × 平均受益者数
+ 查询临时数据与审计索引Raw 按 provider、billing period 和 delivery scope 分区;normalized 按 charge period 与 provider 分区;高频报表读取聚合物化层,不反复扫描完整 raw。裁剪列之前先检查 Dataset Completeness、发票对账、分摊键和退出需求。
长期指标包括 delivery_lag、raw_to_normalized_row_delta、cost_conservation_delta、required_field_failure、unknown_resource_ratio、extension_column_drift、late_correction_amount 和 adapter_version_distribution。指标必须按 provider 与 schema version 分组,否则一家云的正常数据会掩盖另一家断档。
升级、回滚与退出
规范升级和供应商 schema 升级分开处理。先更新合同与 adapter,在 shadow 表回放代表性窗口;确认金额守恒、NULL 语义、扩展列、修正处理和下游兼容后,再把读取视图切到新版本。出现异常时恢复旧视图与旧 adapter,raw 数据不回滚也不删除。
退出某个规范化平台时,导出 raw manifest、schema registry、映射规则、adapter 版本、normalized 表、质量事件、发布水位和 lineage。替代实现用同一 raw 批次并行计算,按 source charge、provider total、invoice 和 owner allocation 四层比较。切换后停止旧调度,撤销云账单与数仓身份,清理临时表和缓存;历史账单按财务保留策略处置,不能因为停用工具而直接删除。
多云成本统一的成功标准不是“所有列都一样”,而是同一问题可以用共同语义查询,任何金额又都能回到供应商交付、修正方式和源费用行。统一层负责降低分析摩擦,原始层和扩展层负责保住事实边界,这两件事必须同时成立。
