3. 数据契约的构成要素:Schema定义、语义约束、SLA承诺、版本管理
一份数据契约到底长什么样?说白了,它不是一个文档,而是一组可执行、可验证的约定。我参与过的项目里,很多人把契约理解成“接口文档”,这其实只对了一半。真正的数据契约,必须包含四个核心要素:Schema定义、语义约束、SLA承诺、版本管理。缺一个,契约就容易变成一纸空文。
核心观点:数据契约不是“写下来就完事”的协议,而是“写下来就能自动检查”的规则。四个要素缺一不可。
3.1 Schema 定义:数据长什么样
Schema 是契约最基础的部分。它告诉所有人:这个数据集有哪些字段,每个字段是什么类型,结构是扁平还是嵌套。我个人习惯用 Avro Schema 或 Protobuf 来定义,因为它们自带 IDL,能直接生成代码。
举个例子,一个订单数据的 Schema 大概长这样:
{
"type": "record",
"name": "Order",
"fields": [
{"name": "order_id", "type": "string"},
{"name": "user_id", "type": "string"},
{"name": "amount", "type": "double"},
{"name": "status", "type": "string"},
{"name": "created_at", "type": "long"}
]
}
嗯,这里要注意:字段类型必须精确。我在项目中遇到过,有人把金额字段定义成 float,结果精度丢失,对账差了十几万。从那以后,金额我统一用 decimal 或 long(单位分)。
避坑指南:我曾经在一个跨团队项目中,发现两个团队对“用户ID”字段的理解完全不一样——一个认为是字符串,另一个认为是整数。后来我们强制要求 Schema 里必须写清楚 logicalType,比如 {"type": "string", "logicalType": "uuid"}。从此再没出过歧义。
3.2 语义约束:值是否合法
光有 Schema 还不够。你想想看,status 字段是字符串,但“已支付”、“已发货”、“已取消”这些值,总得有个范围吧?语义约束就是干这个的。
常见的语义约束包括:
- 枚举值:字段只能取特定值,比如
status IN ('pending', 'paid', 'shipped', 'cancelled') - 范围限制:数值必须在某个区间,比如
amount >= 0 - 格式校验:正则表达式,比如手机号、邮箱格式
- 业务规则:跨字段逻辑,比如
IF status='paid' THEN amount > 0
我建议把语义约束写在 Schema 的 doc 字段里,或者用单独的约束文件管理。举个例子:
{
"name": "status",
"type": "string",
"doc": "订单状态枚举: pending|paid|shipped|cancelled",
"constraints": {
"enum": ["pending", "paid", "shipped", "cancelled"]
}
}
注意:语义约束不能只写在文档里,必须可自动化校验。我曾经见过一个项目,约束写了一大堆,但没人去实现校验逻辑,结果数据照样乱传。后来我们引入了 Great Expectations 做自动校验,每次数据交换前先跑一遍约束检查,不合格的直接拒绝。
3.3 SLA 承诺:什么时候给,质量如何
数据契约不只是“数据长什么样”,还得说清楚“什么时候给”、“给多久”、“质量多高”。这部分就是 SLA(Service Level Agreement)。
我参与过的一个实时推荐项目,数据提供方承诺“延迟不超过 5 秒”,但实际经常跑到 30 秒。因为没有 SLA 约束,消费方只能干等。后来我们在契约里明确写了:
| SLA 指标 | 承诺值 | 测量方式 | 违约后果 |
|---|---|---|---|
| 数据延迟 | P99 < 5s | 端到端时间戳 | 自动降级到离线数据 |
| 可用性 | 99.9% | 每分钟健康检查 | 触发告警 + 补偿 |
| 数据完整性 | 100% 无丢失 | 记录数比对 | 重新推送 + 审计 |
| Schema 合规率 | 100% | 自动校验 | 拒绝写入 |
说白了,SLA 是契约里最有“法律效力”的部分。没有 SLA,契约就是一张废纸。
我的经验:SLA 承诺要可测量、可验证。不要写“尽快”、“尽量保证”这种模糊词。每个指标都要有明确的测量方式和阈值。我曾经帮一个团队把 SLA 从“实时”改成“P99 < 3s”,结果双方都松了一口气——因为终于知道底线在哪了。
3.4 版本管理:变了怎么办
数据契约不是一成不变的。业务在变,Schema 在变,约束在变,SLA 也在变。怎么管理这些变化?版本管理就是答案。
我建议采用 SemVer(语义化版本) 来管理契约版本:
- MAJOR:不兼容的变更,比如删除字段、修改类型
- MINOR:向后兼容的变更,比如新增可选字段
- PATCH:不影响 Schema 的变更,比如修改文档、调整约束
举个例子:
# 契约版本: 1.2.0
# 变更记录:
# 1.0.0 - 初始版本
# 1.1.0 - 新增 discount 字段(可选)
# 1.2.0 - 新增 shipping_address 字段(可选)
# 2.0.0 - 废弃 user_id 字段,改用 user_uuid
避坑指南:我曾经见过一个团队,版本号一直用 v1、v2,但没人知道 v2 到底改了啥。后来我们强制要求每个版本必须附带 CHANGELOG,并且用 Git 管理契约文件。每次变更都要走 PR 评审,评审通过后才能发布新版本。
版本管理的另一个关键是兼容性检查。我建议在 CI/CD 流程中加入自动兼容性检查:
# 伪代码示例
def check_compatibility(old_schema, new_schema):
if old_schema.has_field('user_id') and not new_schema.has_field('user_id'):
raise IncompatibleChange("删除字段 user_id 属于 MAJOR 变更")
if old_schema.field('amount').type != new_schema.field('amount').type:
raise IncompatibleChange("修改字段类型属于 MAJOR 变更")
# ... 其他检查
重要提醒:版本管理不只是“改个数字”。它需要配套的废弃策略。比如,一个字段要废弃,必须先标记为 deprecated,保留至少两个大版本周期,然后才能删除。我习惯在 Schema 里加一个 deprecated 标记:
{
"name": "old_field",
"type": "string",
"doc": "已废弃,请使用 new_field",
"deprecated": true
}
3.5 四要素如何协同工作
这四个要素不是孤立的。它们共同构成一份完整的数据契约:
- Schema 定义 是骨架,告诉数据长什么样
- 语义约束 是血肉,告诉数据值是否合法
- SLA 承诺 是契约的“法律条款”,告诉双方责任边界
- 版本管理 是时间轴,让契约能随着业务演进
我习惯把四要素放在同一个 YAML 文件里管理:
# data_contract.yaml
version: "1.2.0"
schema:
type: record
name: Order
fields:
- name: order_id
type: string
- name: amount
type: double
constraints:
min: 0
- name: status
type: string
constraints:
enum: ["pending", "paid", "shipped", "cancelled"]
sla:
latency_p99: "5s"
availability: "99.9%"
completeness: "100%"
changelog:
- version: "1.0.0"
date: "2024-01-01"
changes: "初始版本"
- version: "1.1.0"
date: "2024-03-01"
changes: "新增 discount 字段"
- version: "1.2.0"
date: "2024-06-01"
changes: "新增 shipping_address 字段"
嗯,这样一份契约,既能给人看,也能给机器执行。你想想看,当数据提供方和消费方都基于同一份契约来开发、测试、部署时,跨域协作的效率会提升多少?
总结一下:数据契约的四个要素——Schema 定义、语义约束、SLA 承诺、版本管理——缺一不可。它们共同定义了数据的结构、合法性、交付承诺和变更规则。没有这四个要素,数据契约就只是一份“看起来很美”的文档,而不是真正能驱动跨域协作的工程工具。