4. Schema 兼容性规则

兼容性规则,说白了就是 Schema 演进的「交通规则」。你改了 Schema,消费者能不能继续跑?生产者能不能发新数据?这些规则就是用来回答这两个问题的。

我刚开始接触 Schema Registry 时,觉得这玩意儿不就是个版本管理吗?后来线上出过一次事故——改了字段类型,老消费者直接反序列化失败。嗯,从那以后,我再也不敢轻视兼容性配置了。

4.1 为什么需要兼容性规则?

先看一个场景。你有个用户信息 Schema:

{
  "type": "record",
  "name": "User",
  "fields": [
    {"name": "id", "type": "int"},
    {"name": "name", "type": "string"}
  ]
}

某天你加了个 email 字段。新生产者开始发带 email 的数据。但老消费者还在用旧 Schema 读数据——它不认识 email,直接报错。

这就是兼容性问题。兼容性规则就是告诉 Schema Registry:什么样的变更允许,什么样的变更禁止。

核心原则: 兼容性不是「能不能改」,而是「改了之后,上下游能不能继续工作」。

4.2 四种兼容性类型

Avro 和 Schema Registry 定义了四种兼容性类型。我按使用频率排序来讲。

类型 含义 消费者兼容 生产者兼容 使用场景
BACKWARD 向后兼容 新 Schema 可读旧数据 旧 Schema 可读新数据 最常用,默认规则
FORWARD 向前兼容 旧 Schema 可读新数据 新 Schema 可读旧数据 消费者先升级的场景
FULL 完全兼容 双向兼容 双向兼容 严格管控的 Schema
NONE 不检查兼容性 不保证 不保证 开发/测试环境

4.3 BACKWARD 向后兼容(默认规则)

这是 Schema Registry 的默认配置。我个人习惯在生产环境用这个。

规则: 新 Schema 必须能读取旧 Schema 写入的数据。

说白了,消费者可以先升级,生产者后升级。你想想看,这符合大多数系统的升级节奏——先改读的,再改写的。

允许的操作:

  • 删除字段(消费者忽略多余字段)
  • 添加可选字段(带默认值)
  • 给已有字段添加默认值
  • 将字段类型放宽(int → long)

禁止的操作:

  • 删除带默认值的字段(消费者找不到字段会报错)
  • 添加必填字段(无默认值)
  • 将字段类型收窄(long → int)
我的经验: 添加新字段时,永远给默认值。哪怕你觉得「这个字段肯定有值」,也先给个 null 或空字符串。我曾经因为没给默认值,导致老消费者反序列化时直接抛异常——线上告警响了半小时。

4.4 FORWARD 向前兼容

向前兼容和向后兼容正好反过来。

规则: 旧 Schema 必须能读取新 Schema 写入的数据。

什么时候用?当消费者升级滞后于生产者时。比如你有一个数据管道,生产者先改,消费者慢慢跟上。

允许的操作:

  • 添加字段(消费者忽略不认识的新字段)
  • 删除可选字段
  • 将字段类型收窄

禁止的操作:

  • 删除必填字段
  • 添加必填字段
  • 将字段类型放宽(旧消费者可能读到放大的值)
注意: 向前兼容在实际项目中用得不多。因为大多数场景是消费者先升级。如果你不确定,用 BACKWARD 更安全。

4.5 FULL 完全兼容

FULL 是 BACKWARD + FORWARD 的合集。双向兼容。

规则: 新 Schema 能读旧数据,旧 Schema 也能读新数据。

这听起来很完美,但限制也最多。你想想看,既要能加字段,又要能删字段,还要保证两边都能读——基本上只能做「添加可选字段」这种温和操作。

允许的操作:

  • 添加可选字段(带默认值)
  • 删除可选字段(带默认值)
  • 重新排序字段(Avro 按名称匹配,不依赖顺序)

禁止的操作:

  • 添加或删除必填字段
  • 修改字段类型
  • 修改字段名称
适用场景: 核心公共 Schema,比如用户 ID、订单状态这类被几十个服务引用的字段。我建议用 FULL 兼容性,变更时强制走评审流程。

4.6 NONE 不检查兼容性

NONE 就是「随便改,不检查」。听起来很爽,但风险也最大。

规则: 任何变更都允许,Schema Registry 只做版本号递增,不校验兼容性。

什么时候用?

  • 开发环境、测试环境
  • 一次性数据导入任务
  • 内部工具,不对外暴露

什么时候千万别用?

  • 生产环境,尤其是跨团队协作
  • 有长期存储的历史数据
  • 消费者升级周期长
血的教训: 我曾经在一个项目里用了 NONE 兼容性,觉得「我们团队小,改 Schema 打个招呼就行」。结果有次改了字段类型,忘了通知下游——下游服务直接反序列化失败,数据丢了 3 小时。从那以后,生产环境我再也不敢用 NONE。

4.7 兼容性规则对比图

下面这张图展示了四种兼容性规则的核心差异。我把它画成了流程图,方便你理解。

Schema 兼容性规则对比 四种兼容性规则核心逻辑 Schema 变更 BACKWARD 新 Schema 读旧数据 FORWARD 旧 Schema 读新数据 FULL 双向兼容 NONE 不检查兼容性 允许操作 • 添加可选字段 • 删除字段 • 添加默认值 • 类型放宽 允许操作 • 添加字段 • 删除可选字段 • 类型收窄 • 消费者忽略新字段 允许操作 • 添加可选字段 • 删除可选字段 • 重新排序字段 • 双向兼容 允许操作 • 任何变更 • 不检查兼容性 • 仅版本递增 • 风险最高 建议:生产环境用 BACKWARD,核心 Schema 用 FULL,开发环境用 NONE FORWARD 用于消费者升级滞后的特殊场景 图:四种兼容性规则的核心逻辑与允许操作对比

4.8 如何选择兼容性规则?

我一般按这个思路来选:

  1. 默认选 BACKWARD —— 90% 的场景都适用。消费者先升级,生产者后升级,符合大多数系统的迭代节奏。
  2. 核心公共 Schema 选 FULL —— 被几十个服务引用的 Schema,变更成本极高。FULL 能强制你走评审流程。
  3. 消费者升级滞后选 FORWARD —— 比如你有一个数据管道,生产者先改,消费者慢慢跟上。
  4. 开发/测试环境用 NONE —— 快速迭代,不卡流程。但上线前一定要改成 BACKWARD 或 FULL。
一个小技巧: 如果你不确定选哪个,先设 BACKWARD。等 Schema 稳定了,再考虑要不要升级到 FULL。别一上来就 NONE,除非你想体验线上事故。

4.9 兼容性检查的底层逻辑

Schema Registry 是怎么检查兼容性的?说白了就是对比两个 Schema 的读写能力。

Avro 的 Schema 兼容性检查基于一个核心机制:Reader Schema 和 Writer Schema。

  • Writer Schema:生产者写入数据时用的 Schema
  • Reader Schema:消费者读取数据时用的 Schema

兼容性检查就是看:给定 Writer Schema 和 Reader Schema,Avro 能否正确反序列化。

举个例子:

// 旧 Schema(Writer)
{
  "type": "record",
  "name": "User",
  "fields": [
    {"name": "id", "type": "int"},
    {"name": "name", "type": "string"}
  ]
}

// 新 Schema(Reader)- 添加了 email 字段,带默认值
{
  "type": "record",
  "name": "User",
  "fields": [
    {"name": "id", "type": "int"},
    {"name": "name", "type": "string"},
    {"name": "email", "type": "string", "default": ""}
  ]
}

新 Reader 读旧 Writer 的数据时,email 字段没有值,Avro 会用默认值 "" 填充。这就是 BACKWARD 兼容。

反过来,如果新 Writer 写了带 email 的数据,旧 Reader 读的时候,Avro 会忽略不认识的字段。这就是 FORWARD 兼容。

核心机制: Avro 的兼容性依赖「默认值」和「字段忽略」两个特性。没有默认值的必填字段,就是兼容性的雷区。

4.10 避坑指南

最后分享几个我踩过的坑:

  • 别改字段名 —— Avro 按名称匹配字段。改名相当于删旧字段加新字段,兼容性检查大概率失败。
  • 别删带默认值的字段 —— 你以为有默认值就安全?错。删除字段后,老数据里这个字段还在,但新 Schema 不认识,反序列化会报错。
  • 类型转换要谨慎 —— int 转 long 是安全的(BACKWARD),但 long 转 int 就不行。string 转 bytes 也不行。
  • 枚举类型最坑 —— 枚举加新值是 FORWARD 兼容,但删除枚举值是 BREAKING CHANGE。我建议枚举尽量用 string 替代。
最后提醒: 兼容性规则不是「配置完就完事」的。每次 Schema 变更,都要重新评估兼容性。我见过太多团队:一开始设了 BACKWARD,后来改 Schema 时没注意,直接破坏了兼容性——线上静默丢数据,过了几天才发现。