4. 序列化协议兼容性:JSON Schema、Avro、Protobuf 规则详解

序列化协议兼容性,说白了就是你的数据契约在演进过程中,老版本和新版本能不能互相读懂。我见过太多团队因为序列化不兼容,上线时直接导致全线崩溃。今天咱们就把三种主流协议的兼容规则掰开揉碎讲清楚。

核心观点:兼容性不是协议自带的,是你设计出来的。每种协议都给了你一套规则,用得好是利器,用不好是坑。

4.1 JSON Schema 兼容性规则

JSON Schema 是文本协议,灵活性最高,但兼容性全靠人工约定。我个人习惯把它当作「文档型契约」来用——适合配置、日志、API 响应这类场景。

4.1.1 向后兼容规则

  • 新增字段必须 optional:老消费者读到新数据时,不认识新增字段,但必须能忽略它。所以新增字段必须带 default 或设 required: false。
  • 禁止删除字段:一旦删除,老消费者还在用这个字段,直接报错。我曾在项目中删过一个看似无用的 status 字段,结果下游三个服务同时挂掉——教训深刻。
  • 字段类型只能放宽,不能收紧:比如 number 可以改成 integer 吗?不行。反过来 integer 改成 number 可以。
  • 枚举值只能增加,不能减少:老消费者可能只处理 ["A", "B"],你突然去掉 B,人家就崩了。

4.1.2 向前兼容规则

  • 新消费者必须能读老数据:老数据里没有你新增的字段,所以新增字段必须设 default 值。
  • 字段重命名是破坏性变更:老数据用的是旧字段名,新消费者读不到。我建议用 deprecated 标记旧字段,等所有消费者都升级后再删除。
我的经验:JSON Schema 的兼容性检查最好自动化。我写过一个 CI 脚本,每次 PR 都对比新旧 Schema,发现破坏性变更直接阻断合并。别指望人工 review 能发现所有问题。

4.2 Avro Schema 兼容性规则

Avro 是我在 Kafka 场景下的首选。它原生支持 Schema 注册表和兼容性检查,规则比 JSON Schema 严格得多。你想想看,Avro 的二进制编码依赖 Schema 来解析,所以兼容性必须精确到字段级别。

4.2.1 四种兼容模式

模式 说明 适用场景
BACKWARD 新 Schema 可以读老数据 消费者先升级,生产者后升级
FORWARD 老 Schema 可以读新数据 生产者先升级,消费者后升级
FULL 同时满足 BACKWARD 和 FORWARD 生产者和消费者任意顺序升级
NONE 不检查兼容性 开发环境或一次性数据

4.2.2 具体规则

  • 新增字段必须有 default 值:Avro 要求所有新增字段必须设 default,否则不兼容。这和 JSON Schema 类似,但 Avro 是强制性的。
  • 删除字段只能删除有 default 的字段:老数据里这个字段的值会被 default 替代。我建议删除前先确认所有消费者都不再使用该字段。
  • 字段类型变更规则:Avro 支持类型提升(promotion),比如 int 可以升为 long,float 可以升为 double。但 string 不能改成 int。
  • 枚举值只能追加到末尾:Avro 的枚举是按索引编码的,中间插入或删除都会导致索引错乱。我曾经在枚举中间加了一个值,结果所有老数据都解析成了错误的值——血的教训。
注意:Avro 的 union 类型变更要特别小心。比如从 ["null", "string"] 改成 ["null", "string", "int"],这是向前兼容的,但反过来就不行。

4.3 Protobuf 兼容性规则

Protobuf 是 gRPC 生态的标配。它的兼容性规则和 Avro 很像,但细节上有很多差异。我个人觉得 Protobuf 的规则更「工程化」——它明确告诉你哪些是 safe 的,哪些是 breaking 的。

4.3.1 核心规则

  • 字段编号(field number)一旦分配,永远不能改:Protobuf 用编号而不是字段名来编码。改编号等于重新定义了一个字段。我见过有人重构时顺手改了编号,结果线上数据全乱了。
  • 新增字段必须用新的编号:不能复用已删除字段的编号。Protobuf 官方建议把删除的字段编号标记为 reserved,防止未来误用。
  • 字段类型变更限制:Protobuf 只允许少数类型变更,比如 int32、uint32、int64、uint64、bool、enum 之间可以互转(但要注意值范围)。string 和 bytes 也可以互转。其他类型变更都是破坏性的。
  • optional 和 repeated 的变更:optional 字段可以改成 repeated 吗?不行。反过来也不行。但 repeated 可以改成 packed repeated,这是兼容的。

4.3.2 oneof 和 map 的特殊规则

  • oneof 字段不能随意增删:往 oneof 里加新字段是兼容的,但删除 oneof 里的字段是破坏性的。老消费者可能还在用那个字段。
  • map 字段的 key 和 value 类型不能改:map 本质上是个特殊字段,key 和 value 的类型变更都是破坏性的。
避坑指南:我曾经在 Protobuf 里把一个 int32 字段改成 uint32,以为只是符号位不同。结果老数据里有负数,新消费者读到后直接溢出成很大的正数。所以类型变更一定要确认值范围完全兼容。

4.4 三种协议兼容性对比

维度 JSON Schema Avro Schema Protobuf
编码方式 文本(JSON) 二进制 二进制
Schema 注册表 无原生支持 Confluent Schema Registry 无原生支持(可自建)
兼容性检查 人工约定 自动检查(4种模式) 人工 + 工具辅助
字段编号 无 无 有(必须唯一且不变)
默认值 可选 必须 可选(proto3 无自定义默认值)
枚举变更 只能追加 只能追加到末尾 只能追加(编号不能复用)

4.5 知识体系总览

下面这张图把三种协议的兼容性规则串起来了。你可以看到,核心逻辑其实就两条:新增字段要安全,删除字段要谨慎。但每种协议的实现细节完全不同。

序列化协议兼容性规则总览 JSON Schema Avro Schema Protobuf 规则要点 • 新增字段必须 optional + default • 禁止删除字段 • 类型只能放宽不能收紧 • 枚举只能追加 规则要点 • 新增字段必须有 default • 删除字段只能删有 default 的 • 支持类型提升(int→long) • 枚举只能追加到末尾 规则要点 • 字段编号永远不能改 • 新增字段用新编号 • 类型变更有限制 • oneof/map 有特殊规则 核心逻辑:新增字段要安全,删除字段要谨慎 但每种协议的实现细节完全不同,需要针对性设计

4.6 我的选择建议

说了这么多规则,到底该选哪个?我个人的经验是这样的:

  • API 网关、配置中心:用 JSON Schema。灵活,人类可读,适合频繁变更的场景。
  • Kafka 消息、事件流:用 Avro + Schema Registry。自动兼容性检查能省掉大量人工 review 时间。
  • gRPC 服务、微服务间调用:用 Protobuf。性能好,工具链成熟,字段编号机制让兼容性更可控。
最后提醒一句:不管选哪种协议,兼容性测试一定要写进 CI/CD 流程。我见过太多「上线前手动检查一下」的团队,最后都出过事。自动化检查不贵,但线上故障的代价你承受不起。