TT Lab
开始
学习 学习路径 课程

数据流水线

给变更编号,让两边都活着

在 TT Lab 中继续学习

目标

创建契约工具 contract.py,用带版本号的 schema 处理 JSON Lines。把版本之间的变化分门别类,按 Avro 的 schema 解析规则判定向后兼容和向前兼容,并用一个读取 schema 度过多个版本混在一起流动的过渡期。

为什么重要

写入方和读取方不会在同一时刻改变。其间必然有一段只有一方是新代码的时期,而在这段时期里数据也仍在持续流动。所以设计 schema 变更时该问的,不是“这个变更对不对”,而是“无论按什么顺序部署,它都能存活吗”。 答案分为三种。增加带默认值的字段在两个方向上都是安全的;增加没有默认值的必填字段,会让新的读取代码读不了旧数据;类型变更中,加宽和收窄的结果恰好相反。改名单看 schema 就是一次新增和一次删除,只有靠别名才能还原成一个事件。但别名只在读取方 schema 里起作用,所以改名只在一个方向上存活。 度过过渡期的办法只有一个。给读取 schema 充分加上默认值,让旧版本也能被读出来。本实验用数字展示这一个默认值能救回多少条。 评分器不会相信你写出来的文字。它会在临时目录里摆好评分器生成的 schema 和行,真正运行你的工具,把分类和判定结果与评分器另行实现的值相比对。字段名、类型和金额每次运行都会变化。

步骤

  1. 创建并运行 /root/evolve/gen_stream.py,在 /root/evolve/schemas 中生成 v1.json 到 v5.json,在 /root/evolve/stream 中生成每个版本各 12 行的 v1.jsonl 到 v5.jsonl,以及五个版本混在一起的 mixed.jsonl。
  2. 在 /root/evolve/contract.py 中实现 fields <스키마>(占位符为 schema),输出版本号、字段名、必填和可选,以及默认值。
  3. 加上 diff <옛 스키마> <새 스키마>(占位符依次为旧 schema、新 schema),对新增和删除进行分类。新增要分成带默认值的和不带默认值的。
  4. 让 diff 根据新版本的 aliases 把改名合并成一个事件。被合并的名字要从新增和删除列表中去掉。
  5. 让 diff 把类型变更分成加宽和收窄。在提升表里就是加宽,不在就是收窄。
  6. 加上 compat <옛 스키마> <새 스키마>,判定向后兼容和向前兼容,并用固定的代码给出原因。
  7. 加上 read <읽기 스키마> <스키마폴더> <파일>(占位符依次为读取 schema、schema 目录、文件),把多个版本混在一起的行按一个版本读出来,再创建宽松读取 schema /root/evolve/reader.json,把两种读取的差异写进 /root/evolve/window.json。
  8. 把五个版本的历史写成 /root/evolve/evolve_report.json 和 /root/evolve/evolve_report.md。

参考

生成并输出五个版本

创建并运行 /root/evolve/gen_stream.py,在 /root/evolve/schemas 中生成 v1.json 到 v5.json,在 /root/evolve/stream 中生成 v1.jsonl 到 v5.jsonl,以及五个版本混在一起的 mixed.jsonl。

每个版本只让一个地方发生变化,之后就能看清判定抓住的是什么。v2 增加带默认值的字段,v3 增加没有默认值的字段,v4 在改名(加上别名)的同时加宽类型,v5 再把这个类型收窄回去。每一行都用 _v 写明是哪个版本。

用默认值区分必填和可选

在 /root/evolve/contract.py 中实现 fields <스키마>(占位符为 schema),以 JSON 输出 version、names、required、optional、defaults。带默认值的字段是可选的,没有默认值的字段是必填的。

names 按声明顺序原样输出,required 和 optional 排序后输出。只需要看有没有 default 键,默认值即使是空字符串或 0,也是可选——按键是否存在来区分,而不是按值。

区分新增的和消失的

加上 diff <옛 스키마> <새 스키마>(占位符依次为旧 schema、新 schema),输出 added_with_default、added_required、removed 三个列表。三个列表都要排序后输出。

只需求名字集合的差。新增的字段,按新版本声明中有没有 default 键分成两类。这一步还可以先不看别名和类型变更。

把改名合并成一个事件

给 diff 的响应加上 renamed。如果新版本独有字段的 aliases 中包含旧版本独有的名字,这两者就是同一个字段。被合并的名字要从 added_* 和 removed 中去掉。

单看 schema,改名就是一次新增和一次删除。别名是把这两者还原成一个事件的唯一机制。不要用值的样本去猜——在这里,写别名的一方是我们,没有猜的理由。

区分加宽和收窄

给 diff 的响应加上 widened 和 narrowed。条目是 [새이름, 옛타입, 새타입](占位符依次为新名字、旧类型、新类型),如果旧类型可以提升为新类型,就是加宽,其他的是收窄。改名的同时类型也变了的字段也要看。

把提升表写成一个字典,判定就只要一行。int 到 long、float、double,long 到 float、double,float 到 double。相同类型也要视为提升,后面的兼容判定才会简单。

分别判定向后和向前

加上 compat <옛 스키마> <새 스키마>(占位符依次为旧 schema、新 schema),输出 backward、forward、reasons。原因只使用参考一节里的四种代码,并排序后输出。

写一个函数,判断读取方能否读取写入方的数据,再把参数对调,得到两个方向。向后兼容是读取方为新版本,向前兼容是读取方为旧版本。别名只使用读取方 schema 的,这一点决定了这里的结果。

用一个读取 schema 度过过渡期

加上 read <읽기 스키마> <스키마폴더> <파일>(占位符依次为读取 schema、schema 目录、文件),再创建能读五个版本的宽松读取 schema /root/evolve/reader.json。把用 schemas/v5.json 读出的结果和用 reader.json 读出的结果,以 strict、tolerant、amount_total 写入 /root/evolve/window.json。

严格读取 schema 会因为没有默认值的字段而把旧版本整批丢掉。给这个字段加上默认值后能救回多少条,就是这一步的答案。别名也要放在读取方,才能沿着旧名字读下去。amount_total 写宽松一方的值。

把版本历史留成一页

对相邻的每一对版本运行 compat,在 /root/evolve/evolve_report.json 中写入 versions、steps、full、broken,并在 /root/evolve/evolve_report.md 中写成四节,标题是 ## 어떤 판이 있나(韩文,意为“有哪些版本”)、## 어느 방향이 깨지나(韩文,意为“哪个方向会被破坏”)、## 전환 기간을 어떻게 넘기나(韩文,意为“如何度过过渡期”)、## 다음 판에 지킬 것(韩文,意为“下一个版本要遵守的事项”)。

steps 是 {"from": 정수, "to": 정수, "backward": 참거짓, "forward": 참거짓, "reasons": [...]} 的列表(占位符依次为整数、整数、布尔值、布尔值)。full 放两个方向都成立的版本对,broken 放至少有一个方向被破坏的版本对,都写成 [옛판, 새판](占位符依次为旧版本、新版本)。报告里要用数字写出过渡期内救回的条数。