数据契约、时间语义与重建:schema 兼容不等于语义兼容
JSON 解析成功不代表"金额"还是同一个意思——int cents 改成 long micros,旧消费者会把 100 分读成 100 微单位。本页定义数据契约的完整范围、事件时间与处理时间的分界、以及"重建完成"的可验证条件。迁移状态机的工程步骤见零停机 schema 迁移;CDC 快照衔接见CDC、事件和投影。
契约比字段名更多
数据 schema 只规定字段形状;消费者还依赖字段含义、单位、空值含义、默认值、枚举扩展和生成条件。序列化兼容测试能发现一部分读写不兼容,不能证明两个版本对"已取消""未知"或"金额"的业务解释一致。字段更名、单位变化和状态合并都应视为语义迁移。100表示分还是元的变更可能比新增字段危险;历史消费者也可能把未知枚举默认为成功。schema可解析不等于业务含义兼容。
事件的时间语义
事件至少需要可区分的事件身份、实体身份、业务版本、发生时间和 schema 版本。事件时间表示业务事实发生的时间;处理时间表示某个消费者处理它的时间;源位点表示它在特定日志或分区中的位置。时钟不同步时不能仅靠时间戳建立全局顺序。单实体更新可用单调版本拒绝旧事件;跨分区因果关系需额外关联信息。
事件发生时间、服务接收时间、数据库提交时间和处理时间分别回答不同问题。迟到事件按哪一时刻计入报表、窗口何时关闭、修订如何发布都要定义;给每条记录加timestamp不提供全局顺序。对象版本宜由权威写入生成,不能依赖不同机器墙钟决定覆盖。
Avro的读写schema与默认值
以Avro1.11.3规范的schema resolution为固定参考,writer与reader schema共同决定读取。reader新增字段需适当默认才能读取缺字段的旧数据;默认值不是强迫所有写者可以省略字段的统一规则。重命名、union和枚举还需按规范验证,而非只跑一次JSON解析。
教学变更:金额int cents→long micros。即使数字类型可提升,单位变更仍需新字段/版本和明确转换;不能把旧100分解释成100微单位。兼容矩阵至少包含旧写新读、新写旧读和历史重放,不能只测新服务互调。
在线迁移是具有兼容窗口的状态机
Expand-and-contract 可建模为阶段状态:旧 schema 唯一写入、兼容 schema 已部署、新旧字段并行维护、回填追平、读取切换、旧写者清退、旧结构删除。每次转换都需要守卫条件,例如所有活跃写者已升级、回填位点已追平、关键键空间差异为零、回滚时间窗已结束。把步骤写在发布脚本里不等于自动满足守卫。
回填和实时写入竞态需要确定胜者。若旧值在扫描后被新写覆盖,而回填随后写入旧副本,数据会倒退;可用版本条件更新、源日志水位或短暂双写协议避免覆盖。迁移要按可重试的确定性分块推进,并持久记录分块进度;分块身份应稳定,重复执行结果应幂等。
切流后发现差异时,是否能回滚取决于新数据能否无损映射回旧模型。若新模型允许旧模型无法表示的状态,简单切换流量会丢弃信息;此时需前向修复或双版本读取窗口。回滚代码与回滚数据是两个独立决定,必须分别界定。
重建必须有可验证的结束条件
重建能力取决于输入是否完整且转换是否可重复。快照和 CDC 日志必须有明确的一致性边界,不能在没有水位衔接的情况下先拷贝表、再从任意位置消费日志,否则两者之间可能形成缺口或重复。投影代码升级后,应能识别输出属于哪个转换版本,并决定全量重放、增量修复或并行重建。
按源快照+水位重建查询视图,保存转换代码版本、schema版本和删除墓碑。完成条件是追上指定截止水位并通过业务校验,而不是"消费者暂时没消息"。源历史不足时需要有权限的快照/基线,不能从半段日志重建完整账本。
假设每日10亿事件,每条压缩后200B,30天仅事件约6TB十进制,还未计副本索引;重放以每秒5万净事件处理,30天300亿条约需6.94天。这个恢复时间会直接影响保留、预计算和热备选择。脚本重建快不代表在线依赖承载得住同等压力。
验证产物
准备不同schema/单位、迟到、重复、删除与无默认字段样本,标明每对reader/writer预期。对重建结果按同水位比较账户汇总、对象版本和权限,记录无法恢复的缺失范围。没有完整来源时应承认未验证,而不是编一个"最终一致"结论。
上下文映射与防腐层契约
本页前面讲"契约比字段名更多",还缺一层:跨上下文的契约由谁维护、变化如何被隔离。答案是上下文映射里的两种结构——发布语言定义"对外说什么",防腐层定义"对内怎么翻译"。
发布语言是上游对外的稳定契约:字段、单位、版本、兼容规则。它不等于上游的内部模型,也不要求下游理解上游的全部语义。防腐层是下游的转换层:把上游的发布语言翻译成自己上下文的模型,隔离上游变化;没有防腐层,下游就被迫遵奉上游模型,上游一改,下游跟着改。
反例:报表上下文直接消费订单上下文的内部事件(含 cost_price_micros),没有防腐层。上游把成本单位从 micros 改成 cents 时,报表金额整体放大一万倍,而接口、消息格式都没变——序列化兼容测试发现不了,只有业务对账能发现。这正是本页开头"100 分读成 100 微单位"的上下文映射版本。
防腐层契约要写成可执行的转换表:上游字段 → 下游字段、单位换算、缺失值语义、版本映射、拒绝条件。转换表不是注释,是代码与测试的共同来源;上游新增字段时,转换表默认忽略并在契约测试中提示,而不是自动透传。
执行 python3 labs/context_model.py all:event-layers 证明集成事件只含发布语言字段,migration_order 给出阶段检查,末尾的独立阴性对照 migration-order-blocked 证明契约未就绪时删除被阻断。两者共同支撑"上游可独立演进、下游按自己的节奏升级"这一承诺;脚本不验证真实序列化与网络行为。
两个合理选择:A 下游直接遵奉上游模型,开发最快但每次上游变化都传导;B 发布语言加防腐层,下游稳定但要多维护一层转换与契约测试。本例选 B,并规定:没有转换表的跨上下文依赖,一律视为架构违规,与本页第 2 篇的"直接读表"同级。
架构师变化条件
把迟到窗口从 5 分钟放宽到 24 小时:报表口径、窗口关闭与修订发布全部重定义,下游预计算失效成本上升。若金额从单一币种变为多币种,单位契约升级为"金额+币种+精度"三元组,历史重放需要旧数据补齐币种默认值的显式声明。重建窗口 6.94 天超过业务可接受恢复时间时,需预计算、热备或更细粒度快照——每次变化重算重建时间与保留预算。
闭卷验收
本页 8 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。