4. 数据契约定义与规范
好,咱们进入正题。数据契约到底长什么样?怎么定义它?
很多团队一上来就写 JSON Schema,结果写出来的东西又臭又长,没人看得懂,更没人愿意维护。我见过最夸张的一个契约文件,光字段定义就写了 3000 行,最后连写它的人自己都忘了某个字段是干嘛用的。
说白了,数据契约不是给你写用的。它是生产工具,是上下游之间的“对账本”。所以它的定义必须清晰、简洁、可执行。
4.1 契约的 Schema 定义:骨架要稳
我个人习惯把契约 Schema 分成三层:元信息层、字段定义层、约束层。这三层缺一不可。
核心原则:契约 Schema 必须同时被人类和机器读懂。人类用来对齐业务语义,机器用来做校验和成本归因。
先看一个完整的契约定义示例。嗯,这个例子我改过好几版,是从一个电商实时数仓项目里提炼出来的:
{
"contract_id": "contract_20241015_ods_order_001",
"version": "2.1.0",
"name": "订单实时接入契约",
"domain": "交易域",
"producer": {
"team": "交易中台",
"system": "order_service",
"contact": "zhangsan@company.com"
},
"consumer": {
"team": "数据平台",
"system": "flink_ods_pipeline",
"contact": "lisi@company.com"
},
"schema": {
"fields": [
{
"name": "order_id",
"type": "string",
"description": "订单唯一标识",
"nullable": false,
"primary_key": true,
"cost_weight": 0.15
},
{
"name": "user_id",
"type": "string",
"description": "用户ID",
"nullable": false,
"cost_weight": 0.05
},
{
"name": "order_amount",
"type": "decimal(18,2)",
"description": "订单金额,单位元",
"nullable": false,
"range": [0.01, 999999.99],
"cost_weight": 0.30
}
],
"constraints": {
"row_count_alert": 100000,
"latency_sla_ms": 5000,
"data_volume_mb_per_day": 2048
}
},
"lifecycle": {
"created_at": "2024-01-15",
"effective_start": "2024-02-01",
"effective_end": null,
"review_interval_days": 90,
"deprecation_policy": "notify_consumer_30d_before"
}
}
你看,这个结构里每个字段都有明确的归属。我建议你在实际项目中,把 cost_weight 这个字段加上去。为什么?因为后面做成本归因时,你要知道每个字段到底花了多少钱。没有这个权重,你只能按行数平摊,那误差就大了。
小技巧:cost_weight 的初始值可以按字段的存储体积和计算复杂度来估算。比如一个 string 类型的 order_id 和 decimal 类型的 order_amount,前者的存储开销通常比后者小。但如果你有复杂的 UDF 解析逻辑,那权重就要调高。
4.2 字段级别约束:细节决定成本
字段级别的约束,是数据契约里最容易被人忽略的部分。很多人觉得“字段嘛,定义个类型就行了”。但我在项目中踩过一个大坑——有一次,上游把一个原本是 string 的字段偷偷改成了 int,下游的解析脚本直接崩了,整整影响了 3 个小时的实时链路。
从那以后,我对字段约束的要求就变得非常严格。我建议你至少包含以下几类约束:
- 类型约束:string、int、decimal、timestamp 等,必须精确到小数点位数。
- 空值约束:nullable 必须明确。我曾经见过一个字段,上游说“非空”,结果每天都有 5% 的空值进来,下游的聚合报表全错了。
- 取值范围约束:比如金额字段的 range,或者状态码的枚举列表。这能帮你提前发现异常数据。
- 主键约束:哪些字段联合构成主键?主键是否唯一?这个不写清楚,下游做去重时就会乱套。
- 成本权重:这个我前面提过,每个字段都要有一个 cost_weight,用于后续的成本分摊计算。
避坑指南:我曾经遇到一个团队,把 cost_weight 设成了固定值 1.0,结果所有字段的成本都一样。那成本归因就失去了意义。记住,cost_weight 必须根据实际计算和存储开销来动态调整,至少每个季度 review 一次。
字段约束的校验逻辑,我建议放在数据接入层。比如在 Flink 或者 Kafka Connect 里做前置校验,不符合契约的数据直接进死信队列。这样就不会污染下游的存储和计算资源。
4.3 生命周期管理:契约不是写一次就完事
数据契约最怕什么?最怕写完之后就没人管了。我见过太多契约文件,创建日期是 2022 年,到现在都没更新过。上游的接口改了八版,契约还是老样子。那这契约还有什么用?
所以,生命周期管理是契约能否落地执行的关键。我个人习惯把生命周期分成四个阶段:
| 阶段 | 状态 | 说明 | 成本归因影响 |
|---|---|---|---|
| 创建期 | draft | 契约刚定义,上下游还在对齐 | 不计入成本归因 |
| 生效期 | active | 双方确认,开始执行 | 按契约权重计算成本 |
| 冻结期 | frozen | 上游不再产生新数据,但历史数据仍需保留 | 只计算存储成本,不计计算成本 |
| 废弃期 | deprecated | 数据已迁移或删除,契约归档 | 成本归零 |
你看,每个阶段对成本归因的影响都不一样。如果契约还在 draft 阶段,你就开始算成本,那数据肯定不准。反过来,如果契约已经 deprecated 了,你还按 active 的权重算,那成本就虚高了。
关键动作:我建议你在契约里加上 review_interval_days 字段,比如 90 天。每 90 天自动触发一次 review 提醒,让上下游重新确认契约是否仍然有效。这个机制能帮你避免“僵尸契约”的出现。
另外,废弃策略也很重要。我见过一个项目,上游说“这个接口要下线了”,结果只提前了 3 天通知下游。下游根本来不及迁移,数据直接断流。所以我在契约里强制要求:deprecation_policy 必须写明提前多少天通知。我个人建议至少提前 30 天,给下游留足缓冲时间。
4.4 知识体系总览:一张图看懂契约定义
说了这么多,咱们用一张图把整个契约定义的知识体系串起来。这张图我画了好几次才满意,它把 Schema、字段约束、生命周期三个核心模块的关系讲清楚了:
这张图你看懂了吗?从最顶层的“数据契约定义”出发,往下分成三条线:左边是 Schema 定义,中间是字段约束,右边是生命周期。每个模块下面又有具体的子项。我建议你把这个图打印出来贴在工位上,写契约的时候对照着看,不容易漏东西。
个人经验:我在团队里推行契约定义时,最开始大家都很抗拒,觉得“写这个太麻烦了”。后来我把这张图做成了一张检查清单,每次新建契约时逐项打勾。三个月后,团队里再也没有人抱怨了——因为打勾比写文档轻松多了。