2. Schema兼容性定义:向前兼容、向后兼容、完全兼容、破坏性变更

聊Schema兼容性之前,我想先问一个问题:你改了一个字段类型,结果线上服务挂了,这算不算破坏性变更?

很多人觉得「我加了字段,又没删东西,怎么会挂?」嗯,这里要注意——兼容性不是你觉得,而是消费者觉得。我见过太多次因为「我以为兼容」导致的线上事故了。

2.1 四个核心概念

我们先给这四个术语一个明确的定义。我个人习惯把它们分成两组来看:

术语 定义 一句话记忆
向前兼容 新版本的Schema可以正确读取旧版本产生的数据 新系统能读旧数据
向后兼容 旧版本的Schema可以正确读取新版本产生的数据 旧系统能读新数据
完全兼容 同时满足向前兼容和向后兼容 双向都能读
破坏性变更 变更后,新/旧版本至少有一方无法正常读写数据 改了就不能用了

关键认知: 向前兼容和向后兼容是方向性的概念,不是「好」或「坏」的标签。一个变更可能向前兼容但向后不兼容,反过来也一样。

2.2 向前兼容(Forward Compatibility)

说白了就是:新代码能处理旧数据。

举个例子,你有一个Avro Schema定义了字段 name 和 age。后来你加了一个 email 字段。如果新版本的消费者读到旧版本产生的数据(没有 email 字段),它不会报错,而是给 email 赋一个默认值——这就是向前兼容。

我的经验: 向前兼容最容易出问题的地方是「默认值」。我曾经遇到一个团队给新增字段设置了 null 作为默认值,但下游代码直接调用了 email.length()——空指针异常,线上直接挂了。所以,默认值不是随便给的,你得考虑下游怎么用。

向前兼容的常见场景:

  • 新增可选字段(带默认值)
  • 给已有字段添加新的枚举值(如果消费者不认识,能优雅降级)
  • 放宽字段类型(比如从 int 变成 long)

2.3 向后兼容(Backward Compatibility)

向后兼容正好反过来:旧代码能处理新数据。

你想想看,微服务架构里,服务A升级了,服务B还没升级。如果服务A发了一条新格式的数据给服务B,服务B能不能正常解析?能,就是向后兼容。

向后兼容的常见做法:

  • 只新增字段,不删除、不改类型
  • 新增字段必须有默认值,这样旧消费者读到新数据时,忽略不认识字段即可
  • 枚举值只增不减,且旧消费者能跳过不认识的值

⚠️ 注意: 向后兼容不等于「什么都不做」。旧消费者虽然能跳过不认识字段,但如果序列化框架不支持跳过(比如某些老版本的JSON解析器),还是会崩。我建议你在做兼容性检查时,把序列化框架的版本也纳入考虑。

2.4 完全兼容(Full Compatibility)

完全兼容就是「双向通行」——新系统能读旧数据,旧系统也能读新数据。

这听起来很理想,但实际项目中很难做到。为什么呢?因为完全兼容要求你在两个方向上都做约束。你加一个字段,得保证旧消费者能忽略它(向后兼容),同时新消费者读到旧数据时也能处理缺失字段(向前兼容)。

我个人习惯把完全兼容当作「黄金标准」,但不会强求所有变更都达到这个级别。原因很简单:

  • 完全兼容的变更通常只能做「加法」——加字段、加枚举值
  • 一旦涉及「减法」(删字段、改类型),几乎必然破坏某个方向
  • 过度追求完全兼容会导致Schema膨胀,字段越来越多,没人敢清理

我的建议: 核心链路、跨团队接口、长期存储的数据,尽量保持完全兼容。内部服务、短生命周期数据、明确知道所有消费者都已升级的场景,可以适当放宽。

2.5 破坏性变更(Breaking Change)

破坏性变更,说白了就是「改了之后,有人会挂」。

我曾经在一个项目中,有人把字段 price 从 decimal(10,2) 改成了 decimal(12,4),他觉得「只是精度变高了,没问题」。结果下游有一个老服务用的是固定长度的二进制协议,直接解析失败——因为字节数变了。

常见的破坏性变更:

变更类型 示例 破坏方向
删除字段 删掉 address 字段 向后(旧消费者读新数据时缺字段)
重命名字段 userName → username 双向(新旧都不认识对方的名字)
缩小字段类型 int → short 向前(新消费者读旧数据可能溢出)
修改默认值 默认值从 0 改成 -1 向后(旧消费者读到新数据,语义变了)
移除枚举值 删掉枚举 PENDING 双向(新旧都可能遇到不认识的值)
改变序列化格式 JSON → Protobuf 完全破坏(格式不兼容)

⚠️ 最容易踩的坑: 很多人觉得「加字段肯定安全」。不一定!如果你加的字段是 required(必填),旧消费者读到新数据时,如果序列化框架要求所有必填字段都存在,而旧消费者不认识这个新字段——它就崩了。所以,加字段也要加 optional 字段,或者带默认值。

2.6 一张图理清关系

下面这张SVG图把四个概念的关系画清楚了,你可以对照着看:

Schema兼容性四象限关系图 Schema兼容性四象限 Schema变更 是否破坏读写? 是 否 破坏性变更 新/旧至少一方无法读写 非破坏性变更 需要进一步判断方向 兼容方向判断 向前兼容 新读旧 向后兼容 旧读新 完全兼容 双向兼容 完全兼容 = 向前兼容 + 向后兼容,是最高级别的兼容性要求

2.7 实际项目中的选择策略

说了这么多理论,落地的时候怎么选?我分享几个实战经验:

  1. 先判断是不是破坏性变更——如果是,必须走变更评审流程,通知所有消费者
  2. 非破坏性变更里,优先追求向后兼容——因为生产环境里,消费者升级往往滞后于生产者
  3. 长期存储的数据(比如Kafka的日志、数据库的历史记录)必须向前兼容——否则历史数据就废了
  4. 完全兼容是奢侈品,不是必需品——只在核心接口和跨团队契约上强求

一个小技巧: 我习惯在Schema的注释里标注「兼容性等级」。比如 @compatibility: backward,这样后续维护的人一眼就知道这个字段的变更约束是什么。别小看这个习惯,它能帮你省掉很多排查时间。