文件名没变,里面变了
一句话总结
别人提供的文件,其 schema 会在不通知的情况下变化。所以接收方要把指纹固化下来,并把哪些会被破坏、哪些不会被破坏写死在代码里,由程序自动判定。
为什么需要它
每周一都会收到同名的文件。连续 12 周都没有问题。到了第 13 周,仪表板上的销售额变成了 0。打开文件一看,有一个列名从 amount_krw 变成了 amount。发送方说“整理了一下字段名”。他们没有通知的原因很简单:在他们看来,这并不是会让我们的管道停下来的事。
这种事反复发生,有结构性的原因:发送方只是修改了自己系统的 schema,而接收方一直把它当作契约在使用。这两个事实之间如果没有文档,变更就总是悄悄到来。而且我们无法控制修改的一方。如果是生产数据库,可以让新旧 schema 共存并逐步迁移,而别人的文件没有这样的余地。
所以能做的只有一件事:让管道先发现“已经变了”这个事实。
工作原理
从收到的文件中提取 schema,固化成指纹。指纹中要包含列名和顺序、为每一列推断出的类型,以及值的样本。只把名称和类型连起来做哈希,就得到一个简短的指纹,这个指纹如果与上周不同,就说明有东西变了。
类型推断要先确定尺子:去掉空值,剩下的值全都是整数的样子,就视为 int;包含小数的,视为 float;全部是 YYYY-MM-DD 的,视为 date;其余视为 str。这是推断而不是声明,所以必须把用了什么尺子写在代码里,之后出错时才知道该改哪里。
然后与上周的指纹核对,把变化分成五种。
| 类型 | 看到什么 | 如何识别 |
|---|---|---|
| 新增 | 出现了原来没有的列 | 列名集合的差 |
| 删除 | 原来有的列没有了 | 列名集合的差 |
| 重命名 | 消失了一个,又多出一个 | 消失的列和新出现的列,值的样本是否大量重合 |
| 类型变更 | 同名列的类型变了 | 比较推断出的类型 |
| 语义变更 | 什么都看不到 | 靠 schema 抓不到,只有从值的分布才能看出来 |
用值来识别重命名是关键。只看名称,是消失了一条、新增了一条,共两件;但如果值的样本几乎相同,就是同一列只换了名字。有了这个判定,管道才能回答“加一条映射就行了”。
最后一行是本实验最重要的一点。语义变更靠 schema 检查永远抓不到。金额单位从韩元变成千韩元,列名和类型都没变。检查全部通过,而销售额只剩下千分之一。
在现场相遇的样子
第一,被不认识的列吓得停下来。发送方为了自己的需要多加一列,是常有的事,我们的加载没有理由因此停下来。所以规则的默认值是不认识的列放行。反过来,消失的必需列则中止。这两条是兼容规则的骨架,其余的都落在这两者之间的某处。
第二,不区分必需和可选。没有区分,所有的删除就会被同等对待,结果没有人再去读警告。必需列的清单是由业务决定的,而不是由数据决定的。
第三,把类型变宽和类型被破坏同等对待。int 变成 float,通常只是改成了小数写法,计算照样可以进行。int 变成 str,合计会变成字符串拼接,或者抛出异常。把两者放在同一个等级,警告就没有用了。
第四,不看分布。识别语义变更的唯一办法,是把值的分布与上周比较。如果中位数跳升到三倍以上,或者跌到三分之一以下,就需要人来看。这不是证据,而是线索:也许那一周真的有大额交易集中出现。把线索交给人,是机器分内的事。
实际工作中真正重要的事
- 把指纹和文件一起保存。要说“与上周不同”,上周的情况必须写在某个地方。
- 把兼容规则写死在代码里,而不是文档里。只在文档里的话,没有人读,读了之后每个人的判定也不相同。
- 必需列的清单从业务方取得。工程师独自确定的话,之后没有修改的依据。
- 分布监控是在产生线索。不要停下来,而是告知,判断由人来做。
下一项实验要做什么
运行一个把同样的 30 条订单在七周内只改变 schema 并导出的复现器,建立接收目录,然后把 schema 工具 schema.py 一步步做大:固化指纹,分类新增和删除,用值的样本找出重命名,区分类型变更,用 contract.json 声明兼容规则,自动判定 pass、warn 和 stop。最后,通过值的分布抓出名称和类型都不变而只有单位变了的列,用数字展示 schema 检查看不到什么。