4. Schema Registry与数据契约

在跨团队数据共享场景中,Kafka 主题本身只负责搬运字节流,它并不理解消息体里到底有哪些字段、字段是什么类型、哪些字段可以为空。如果生产团队悄悄把 order_id 从 string 改成 long,或者删掉一个下游正在使用的字段,消费方往往要等到反序列化抛异常或业务逻辑出错时才发现问题。Schema Registry 的作用,就是把“消息长什么样”这件事从口头约定变成可注册、可校验、可版本化的数据契约。

Schema Registry 是一个独立于 Broker 的 HTTP 服务,通常与 Kafka 集群配套部署。生产者发送消息前,把消息使用的 Schema 注册到 Registry,拿到一个全局唯一的 Schema ID;实际写入 Kafka 的消息体采用“Magic Byte + Schema ID + 序列化数据”的格式,消费方读取时先取出 Schema ID,再向 Registry 拉取对应 Schema 完成反序列化。这样,Schema 本身不必随每条消息重复传输,既节省带宽,又保证收发双方对同一份契约达成一致。

Schema Registry 支持三种主流格式,它们的表达能力与演进特性差异明显,选型时不能只看“哪个流行”。

  • Avro:以 JSON 描述 Schema,支持丰富的基本类型、record、enum、array、map、union,并可为字段设置 default。它是三者中与兼容性检查配合最成熟的格式,适合字段频繁增删、需要严格演进控制的场景。
  • Protobuf:使用 .proto 文件定义消息,字段以编号标识,天然支持向前向后兼容,序列化体积小、性能好,适合跨语言、高吞吐的服务间通信。但字段编号一旦分配就不应复用,删除字段需用 reserved 标记。
  • JSON Schema:以 JSON 描述 JSON 文档结构,人类可读性最好,适合已有 JSON 生态、对性能不敏感的场景。但它对默认值和可选字段的演进约束相对宽松,容易在“看起来没变”的情况下破坏兼容性。

注册 Schema 时,Registry 会先做兼容性检查,再决定是否接受新版本。兼容性策略决定了“新 Schema 能否替换旧 Schema”,常见的有以下几种:

策略含义典型适用场景
BACKWARD新 Schema 可读旧数据,消费者先升级默认策略,消费方主导演进
FORWARD旧 Schema 可读新数据,生产者先升级生产方先发布新字段
FULL双向兼容,新旧互读要求最严格,升级顺序自由
NONE不做兼容性检查仅限实验或完全受控场景

以 Avro 为例,BACKWARD 兼容允许“新增带默认值的字段”和“删除字段”,但不允许“新增无默认值字段”或“修改字段类型”。FULL 兼容则要求新增字段必须带默认值,且删除字段也必须谨慎,因为旧消费者可能仍依赖它。理解这些规则,才能在设计契约时预判哪些改动会被 Registry 拒绝。

兼容性检查发生在 Schema 注册阶段,而不是消息发送阶段。一旦注册被拒绝,生产者就无法用新 Schema 写入,从而在源头拦截破坏性变更。

下面这张图展示了 Schema Registry 在生产者、Broker、消费者之间的位置,以及 Schema ID 如何贯穿消息的序列化与反序列化过程。

生产者 Avro 序列化器 获取 Schema ID 写入消息 Schema Registry Schema 存储 兼容性检查 版本管理 Schema ID 分配 Kafka Broker Topic 分区日志 Magic + ID + 数据 消费者 Avro 反序列化器 按 ID 拉取 Schema 还原业务对象 注册/查询 返回 Schema ID 发送序列化消息 拉取消息 按 ID 查询 Schema 消息格式: [Magic Byte 0x0] [4 字节 Schema ID] [Avro/Protobuf/JSON 序列化数据]

跨团队演进时,最稳妥的做法是“先约定兼容策略,再设计 Schema,最后才写代码”。例如,订单团队要新增 coupon_code 字段,若主题采用 BACKWARD 策略,则新字段必须带默认值 "",这样旧消费者读到新消息时,缺失字段会回退到默认值,不会报错。若采用 FORWARD 策略,则旧消费者无法读取新字段,但新消费者可以读取旧消息,适合生产方先升级、消费方后跟进的节奏。

实际注册时,可以通过 REST API 直接操作。以下示例把一份 Avro Schema 注册到 orders-value 主题,并指定 BACKWARD 兼容:

curl -X POST http://registry:8081/subjects/orders-value/versions \
  -H "Content-Type: application/vnd.schemaregistry.v1+json" \
  -d '{
    "schema": "{\"type\":\"record\",\"name\":\"Order\",\"fields\":[{\"name\":\"order_id\",\"type\":\"string\"},{\"name\":\"amount\",\"type\":\"double\"},{\"name\":\"coupon_code\",\"type\":\"string\",\"default\":\"\"}]}"
  }'

# 返回示例
{"id": 12}

如果新 Schema 违反兼容策略,Registry 会返回 409 错误,并附带不兼容的具体原因,例如“新增字段 coupon_code 没有默认值”。这时生产者必须调整 Schema,而不是绕过检查。对于已经上线的主题,建议把兼容策略设置为 FULL,并在 CI 中把 Schema 文件纳入版本控制,每次变更都先跑一次兼容性校验,再合并发布。

跨团队协作时,建议为每个主题明确“Schema Owner”,并把 Schema 文件放在独立的 Git 仓库中。任何字段变更都通过 Pull Request 评审,评审通过后再由 CI 自动注册到 Registry,避免个人直接改线上契约。

三种格式的选型可以归纳为:需要严格演进控制、字段增删频繁,优先 Avro;需要跨语言、高性能、字段编号稳定,优先 Protobuf;已有 JSON 生态、对可读性要求高,可选 JSON Schema。无论选哪种,核心原则一致:Schema 是团队之间的数据契约,注册是契约生效的入口,兼容性策略是契约演进的护栏。把这三件事固定下来,跨团队的数据共享才能从“靠默契”走向“靠机制”。