Schema 注册中心原理:Avro/Protobuf/JSON Schema 对比、Schema Registry 架构、版本管理机制
好,咱们进入第二章。上一章聊了为什么需要 Schema 治理,这一章咱们直接深入底层——Schema 注册中心到底是个什么东西?它怎么工作的?
说白了,Schema Registry 就是一个中央仓库。它存着所有消息的“身份证”——也就是 Schema 本身。生产者和消费者在发消息、读消息之前,都得先来这里查一下:哎,这个 topic 用的 Schema 长什么样?
我刚开始接触这个的时候,觉得不就是存个 JSON 吗?后来踩了坑才发现,版本管理、兼容性检查、序列化格式差异……每一个都是坑。咱们一个一个说。
Avro / Protobuf / JSON Schema 对比
先解决一个最基础的问题:市面上这么多 Schema 格式,我该选哪个?
我个人的习惯是:先看生态,再看性能,最后看团队熟悉度。下面这张表是我自己整理的对比,你直接拿去用。
| 维度 | Avro | Protobuf | JSON Schema |
|---|---|---|---|
| 序列化格式 | 二进制(紧凑) | 二进制(紧凑) | 文本(JSON) |
| Schema 定义语言 | JSON / IDL | .proto 文件 | JSON |
| 是否需要 IDL 编译器 | 可选(动态模式不需要) | 必须(protoc) | 不需要 |
| Schema 演进(兼容性) | 强(内置兼容性规则) | 强(需手动配置) | 中等(依赖工具) |
| 性能(序列化/反序列化) | 快 | 非常快 | 慢(文本解析) |
| Schema Registry 集成 | Confluent 原生支持 | 需额外适配 | 需额外适配 |
| 典型场景 | Kafka 生态、大数据 | 微服务 gRPC、高性能场景 | REST API、配置文件 |
我的建议:如果你在用 Kafka,无脑选 Avro。Confluent Schema Registry 对 Avro 的支持是最成熟的,版本管理、兼容性检查都是开箱即用。Protobuf 更适合 gRPC 场景。JSON Schema 嘛……说实话,我只有在做 API 文档校验时才用它。
为什么会这样?你想想看,Avro 的 Schema 是自描述的——消息里不存 Schema 本身,只存一个 Schema ID。消费者拿到 ID 后去 Registry 里查。这样消息体非常小,性能也好。Protobuf 虽然也二进制,但它需要双方提前编译好 .proto 文件,动态性差一些。
Schema Registry 架构
好,选定了格式,咱们看看 Registry 本身长什么样。
我画了一张架构图,你一看就明白。
你看这个图,核心就三个角色:生产者、消费者、Registry。Kafka Broker 只是消息管道,它不关心 Schema 是什么。
生产者发消息前,先问 Registry:“我这个 topic 的 Schema 是啥?给我注册一下。” Registry 返回一个全局唯一的 ID。生产者把这个 ID 塞到消息头里,然后发到 Kafka。消费者收到消息,先读 ID,再去 Registry 查 Schema,最后反序列化。
一个小技巧:Registry 本身也是用 Kafka topic 来存 Schema 的(叫 _schemas)。这样做的好处是——高可用、持久化、天然支持事件回溯。我遇到过有人用 MySQL 做后端存储,结果 Schema 一多,查询性能直线下降。Kafka 做存储,稳得很。
版本管理机制
这是 Schema Registry 最核心的能力,也是坑最多的地方。
你想想看,一个 topic 的 Schema 不可能一成不变。今天加个字段,明天改个类型,后天删个字段……如果消费者和生产者的 Schema 不一致,反序列化直接报错。
Registry 怎么解决?版本管理 + 兼容性检查。
每次你注册一个新 Schema,Registry 会检查它和已有版本的兼容性。兼容性策略有几种:
- BACKWARD(向后兼容):新 Schema 能读旧数据。这是最常用的。比如你加了一个 optional 字段,旧数据没有这个字段,新消费者读的时候给个默认值就行。
- FORWARD(向前兼容):旧 Schema 能读新数据。比如你删了一个字段,旧消费者读新数据时忽略它就行。
- FULL(完全兼容):同时满足 BACKWARD 和 FORWARD。说白了就是双向兼容。
- NONE(不检查):随便改,后果自负。我建议你永远不要在生产环境用这个。
我曾经踩过的坑:有一次我改了一个字段的类型,从 int 改成 string,以为加了兼容性检查就没事了。结果 BACKWARD 检查直接报错——因为旧数据是 int,新 Schema 是 string,反序列化时没法自动转换。后来我学乖了:改类型一定要加新字段,废弃旧字段,而不是直接改。
版本管理的具体流程是这样的:
- 生产者提交新 Schema 到 Registry。
- Registry 根据配置的兼容性策略,检查新 Schema 和最新版本(或所有版本)的兼容性。
- 如果兼容,Registry 生成一个新版本号(比如从 1 变成 2),返回 Schema ID。
- 如果不兼容,Registry 拒绝注册,返回错误。
这里有个细节:Schema ID 和版本号是两回事。ID 是全局唯一的,用于消息头;版本号是 topic 级别的,用于管理演进。同一个 Schema 在不同 topic 里可能有不同的版本号,但 ID 是唯一的。
我举个例子你就明白了:
// 版本 1:原始 Schema
{
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"}
]
}
// 版本 2:加了 email 字段(optional)
{
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"},
{"name": "email", "type": ["null", "string"], "default": null}
]
}
版本 2 对版本 1 是 BACKWARD 兼容的——因为新 Schema 能读旧数据(email 字段有 default null)。Registry 会允许注册,版本号变成 2。
但如果你把 id 的类型从 int 改成 long:
// 版本 3:改了 id 类型(不兼容)
{
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "long"}, // 从 int 改成 long
{"name": "name", "type": "string"}
]
}
Registry 会直接拒绝——因为旧数据是 int,新 Schema 是 long,反序列化时没法自动转换。嗯,这就是我前面说的那个坑。
核心原则:Schema 只能加不能改,只能弃不能删。加字段时一定要给 default 值。改类型?想都别想,老老实实加新字段。
最后说一句,版本管理不是 Registry 单方面的事。你需要在生产者和消费者两端都做好兼容性设计。比如消费者要能处理未知字段,生产者要避免发送不兼容的数据。Registry 只是帮你检查,真正跑起来还得靠代码。