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 知识体系总览
下面这张图把三种协议的兼容性规则串起来了。你可以看到,核心逻辑其实就两条:新增字段要安全,删除字段要谨慎。但每种协议的实现细节完全不同。
4.6 我的选择建议
说了这么多规则,到底该选哪个?我个人的经验是这样的:
- API 网关、配置中心:用 JSON Schema。灵活,人类可读,适合频繁变更的场景。
- Kafka 消息、事件流:用 Avro + Schema Registry。自动兼容性检查能省掉大量人工 review 时间。
- gRPC 服务、微服务间调用:用 Protobuf。性能好,工具链成熟,字段编号机制让兼容性更可控。
最后提醒一句:不管选哪种协议,兼容性测试一定要写进 CI/CD 流程。我见过太多「上线前手动检查一下」的团队,最后都出过事。自动化检查不贵,但线上故障的代价你承受不起。