3160 字
约 10 分钟
14
S13-A14 · 数据契约的跨系统治理:字段改了谁受影响

上游把 status 从数字 1/2/3 改成字符串 "PAID"/"REFUNDED",改了映射、跑了单测、发了版。三天后报表口径先崩,第五天风控策略开始误判——下游没人收到通知,而下游代码把这些值当数字做了区间判断。

这和接口不兼容改动有本质区别:接口改动会抛异常,数据改动不会。没有调用栈、没有编译期检查、没有一行报错,只有一份看起来正常但语义已经变了的表。


1. 数据契约与接口契约的四个硬差别

维度 接口契约 数据契约 直接后果
检查时机 编译期或调用时 读取时 错误延迟到消费方
失败形态 抛异常 / 4xx 静默错误值、空值、口径漂移 无告警可依赖
变更可见性 服务方发版即可查 已落历史数据同样受影响 回滚数据极贵
消费方数量 通常可枚举 常不可枚举 通知覆盖率天然低

第三行最容易被低估。接口改错了回滚代码就行;数据改了,历史分区里写进去的旧值不会自己变回来,要么双写一段时间、要么写回填脚本——回填是数据契约治理里最贵的一项。


2. 契约要写死的六件事

要素 不写会怎样 典型事故
字段名 同义不同名(user_id/uid) join 静默丢行
类型 数值与字符串混用 区间判断永为假
枚举取值 新增值下游当未知处理 新状态计入"其他"
空值语义 分不清"没有值"与"值为零" 均值被拉低
唯一性 消费方默认主键唯一 重复计数
更新时效 假设实时,实际 T+1 报表与线上对不上
//─── 契约样例:枚举与空值语义必须显式,不靠约定
contract: order.status
version: 3
owner: trade-platform
fields:
  status:
    type: string
    enum: ["CREATED", "PAID", "REFUNDED"]
    unknown_policy: reject      // reject / pass-through,必须选一个
  paid_at:
    type: timestamp
    nullable: true
    null_means: 未支付        // 空值不是"未知",要写明具体含义
freshness: T+1 04:00          // 更新时效进契约,不进口头约定

unknown_policy 决定新枚举值上线时下游是报错还是放行。两者都可以,但必须选一个并写下——不写,实际行为由每个消费方各自实现决定,同一个新值在不同系统里含义就不同。


3. 血缘:图是画的,不是测的

多数团队都有一张血缘图。它通常是人工填报的:上游在某个平台上勾选下游消费者。这类图有三个典型失真:

失真 成因 后果
漏报 有人直接连库,没走数据平台 图上看不到这个消费方
过时 任务下线了,关系没删 通知发给已无人维护的任务
只到任务级 只记"任务 A 依赖表 T" 不知道依赖的是哪一列

列级血缘才回答"字段改了谁受影响",但它必须测出来而不是填出来:解析 SQL 取列引用、解析作业配置取输入输出、运行期抓实际读取的列。

-- 从查询日志里抽取列级引用,是列级血缘最可靠的来源
SELECT user_id, status, paid_at FROM ods_order WHERE dt = '${bizdate}';
//─── 从 jobs 定义与 SQL 文本批量抽取列引用,作为血缘初稿
$ rg -o -N 'SELECT[^;]+FROM\s+\w+' jobs/ | sort -u > lineage.txt
$ rg -l 'ods_order' jobs/ | wc -l    // 真实引用点数,与血缘图比对

判据是覆盖率:血缘图引用点数 ÷ 实际引用点数。低于八成时,任何"我们通知下游"的承诺都不可信。


4. 发现下游影响的三条路

方式 覆盖率 成本 能发现
列级血缘 中高 中,需解析与日志采集 结构化引用
样本比对 高 低,当天可用 实际消费方(含直连的)
字段使用统计 中 低,靠审计日志 哪些列真被读了

样本比对最被低估,因为它能抓到血缘抓不到的人:

//─── 样本比对:改动前后各取一份样本,看下游哪些表的内容随之变了
$ rg -n 'ods_order|order_status' audit/query-2026*.log | wc -l
$ diff <(cut -f1-4 before.tsv) <(cut -f1-4 after.tsv) | head

实际用法是反过来的:先建立"表内容指纹"的定期快照,字段一改,比对哪些下游表指纹变了。这条路不需要任何人填报血缘,代价是覆盖率依赖快照范围。


5. 字段废弃的顺序与"加字段"的陷阱

-- 正确顺序:三段动作,不是一次上线
-- 第 1 段 停写:上游不再产生新值,旧列仍可读;新列先并存
ALTER TABLE ods_order ADD COLUMN status_v2 VARCHAR(16) NULL COMMENT '并存期';
-- 第 2 段 停读:下游全部切到新列,观察一个完整业务周期
-- 第 3 段 删列:确认无读取后再 DDL,并按 MySQL 版本核实在线 DDL 行为
阶段 动作 最短观察期 跳过后的表现
停写 新旧并存双写 一个业务周期 切换当天发现缺字段
停读 下游分批切读 一个对账周期 对账不平才发现漏切
删列 DDL 删除 确认读取全停 边缘任务突然失败

"先停写"最违反直觉:多数团队先让下游切读、再停上游写。先停写的意义在于——新列的数据完整性可被独立验证,而旧列还在,随时能对账。

"加字段"看起来安全,但它会破坏两类下游:

破坏形式 原因 表现
SELECT * 展开 消费方按位置取值 新列插在中间导致下标错位
位置依赖 导出文件按列序对接 列偏移一位,全表错位
结构假设 下游按"列数固定"校验 校验失败或静默跳过
序列化假设 下游用定长格式解析 截断或解析异常

结论:加字段算兼容改动的前提是"所有消费方都按列名取值"。这个前提被验证之前,加字段与删字段风险同级。


6. 隐式契约:比显式字段更容易出事

隐式契约 谁在依赖它 破坏方式
枚举会遇到的所有值 下游的 if/switch 与策略表 新增值落入"其他"分支
时间精度(秒/日) 下游日期分组 改精度后分组数翻倍
分页语义(是否含边界) 下游的增量拉取 边界行重复或丢失
空表与零行的区别 下游"无数据"判断 有任务无数据被当故障
主键排序相对稳定 下游增量水位 水位推进后漏行
时区 下游日切点 跨日数据归属错

这份清单上每一项都不是"字段",不会有任何契约文档写到它们。它们靠行为约定存在,只能用变更通知保护:任何影响这六项的改动,即使动了零个字段,也要走通知流程。

//─── 变更通知的最小字段集:没有下游清单就不能发
change_id: CHG-2026-0417
contract: order.status
kind: enum_add          // enum_add / type_change / precision_change
new_values: ["CANCELLED"]
affected:               // 必须非空;为空意味着血缘没覆盖,先补血缘
  - risk-strategy
  - report-daily
notice_deadline: 2026-11-20

7. 谁有权改契约

这条必须写死,而且必须写在一句话里:契约的变更权属于数据属主,不属于产出方,也不属于消费方。

角色 能做什么 不能做什么
数据属主 批变更、定义语义、定排期 改语义而不通知
产出方 提变更、做双写与回填 自己批准自己的变更
消费方 提需求、要求观察期 直连库绕过契约
平台方 采血缘、跑通知、校验契约 决定字段语义

"消费方不得直连库"是这套治理的地基:一旦有人直连,血缘与通知的覆盖率就不再是 100%,整条通知链失去意义。要么开只读代理视图并纳入血缘采集,要么明确这些消费者在覆盖范围之外。


8. 度量与边界

指标 计算方式 读数含义
契约覆盖字段比 已登记 ÷ 关键表字段数 治理覆盖面
通知覆盖率 实际触达 ÷ 实际存在 血缘准确率
下游事故数 因数据变更导致的下游故障数 唯一的最终指标
血缘准确率 抽检 N 条正确比例 通知能不能信
回填成本 回填行数或工时 变更的隐性账单

血缘准确率必须靠抽检:抽样十个已知存在的引用,看血缘图里有没有。这动作一小时内能完成,却决定了后面所有通知机制是否值得信任。

场景 建议 理由
单系统内部表 用代码内类型定义 没有跨边界,契约退化为注释
临时中间表、寿命小于一周 不登记 通知成本高于风险
只在一次任务内使用的数据 不登记 消费方唯一且可见
消费方与产出方同团队 用接口契约替代 链路短,契约成本收不回
关键表字段少于二十个 一页 README 代替平台 平台化收益低于维护成本

正式契约治理的成本不在采集工具,而在每次变更都要有人确认受影响清单并等观察期。这个等待期会变成交付节奏的一部分,所以它只适合"改错了要回填几个月数据"的表。改错了只需重跑一次任务的数据,正式契约是过度投入。


9. 排错:五种"改了才知道"

现象 根因 处理
报表数字变小但无报错 枚举新值落入"其他"或空值参与计算 查枚举与空值语义,补 unknown_policy
判断逻辑突然不生效 类型从数值变字符串 比对样本值实际类型
join 后行数变少 字段名或唯一性假设变了 查主键唯一性与重复行
增量任务丢行 分页边界或水位排序变了 用同一水位重跑,比对行数
有任务但无新数据 空表与零行语义被改动 明确"无数据"的表达方式

这些现象的共同点是:报错信息里没有一个字提到契约。排错入口不是日志,而是"这次变更动了什么"——变更通知的字段列表是第一手证据。


10. 自测

  1. 数据改动和接口改动,哪一种更难回滚,为什么?
  2. 契约里 unknown_policy 不写,实际行为由谁决定?
  3. 血缘图准确率该怎么抽检,为什么不能看填报完整度?
  4. 字段废弃的顺序为什么是"先停写、再停读"?
  5. "加字段"在什么前提下才算兼容改动?
  6. 哪六项隐式契约即使动了零个字段也需要走通知流程?
S13-A14 · 数据契约的跨系统治理:字段改了谁受影响
https://www.clxhxhhr.top/posts/6322/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。