3. Schema 定义与版本化:Avro、Protobuf、JSON Schema 简介,Schema 版本号管理策略(语义化版本)

好,我们进入第三章。这一章聊的是数据契约的“骨架”——Schema。说白了,就是定义你的数据长什么样,有哪些字段,每个字段是什么类型。没有 Schema,数据就是一盘散沙,上下游根本没法对齐。

但光有 Schema 还不够。业务在变,字段在加,类型在改,你怎么保证改了之后老系统不崩?这就引出了版本化。我见过太多团队,Schema 改了直接覆盖,下游消费端悄无声息地挂掉,然后排查半天才发现是字段对不上了。嗯,咱们今天就把这事彻底理清楚。

3.1 三种主流 Schema 语言:Avro、Protobuf、JSON Schema

目前业界用得最多的就是这三种。它们各有各的脾气,选型时得看你的场景。

特性 Avro Protobuf JSON Schema
序列化格式 二进制(紧凑) 二进制(紧凑) JSON(文本,可读性强)
Schema 存储 通常与数据一起存储或注册中心 通常预编译进代码 独立文件,运行时解析
Schema 演化 强支持(字段可增删改,有默认值机制) 支持(有编号和规则,但删除字段需谨慎) 支持(通过 allOf/oneOf 等组合,但较复杂)
性能 高(适合大数据批量场景) 高(适合 RPC 和微服务) 中(解析开销大,适合配置类数据)
典型场景 Kafka 消息、Hadoop 生态 gRPC、微服务间通信 API 文档、配置文件、轻量级校验
我的个人习惯: 如果数据要进 Kafka 或数据湖,我首选 Avro。它天生为大数据设计,Schema 注册中心(如 Confluent Schema Registry)配合得天衣无缝。如果是微服务之间的 RPC 调用,Protobuf 更合适,性能好,代码生成也方便。JSON Schema 嘛,我一般用在 API 文档生成和简单的配置校验上,毕竟人类可读,调试起来方便。

3.2 Avro 快速上手

Avro 的 Schema 定义用 JSON 写。举个例子,一个用户信息的数据契约:

{
  "type": "record",
  "name": "User",
  "namespace": "com.example.contract",
  "fields": [
    { "name": "userId", "type": "string" },
    { "name": "userName", "type": "string" },
    { "name": "email", "type": ["null", "string"], "default": null },
    { "name": "age", "type": "int", "default": 0 }
  ]
}

注意看 email 字段,我用了联合类型 ["null", "string"],并且给了默认值 null。这是 Avro 演化友好的关键——新加的字段只要有默认值,老数据反序列化时就不会报错。

我曾经踩过的坑: 有一次我加了一个没有默认值的必填字段,结果下游消费端全部反序列化失败,线上告警炸了。从那以后,我定了一条铁律:新增字段必须提供默认值,除非你能保证所有生产者同时升级。但现实中,同时升级几乎不可能。

3.3 Protobuf 快速上手

Protobuf 的 Schema 定义在 .proto 文件中。同样的用户信息:

syntax = "proto3";

package com.example.contract;

message User {
  string user_id = 1;
  string user_name = 2;
  optional string email = 3;
  int32 age = 4;
}

Protobuf 的每个字段都有一个唯一的编号(= 1、= 2 等)。这个编号一旦发布,就永远不能改。为什么?因为序列化时用的是编号而不是字段名。你改了编号,老数据就全乱了。

另外,proto3 中所有字段都是 optional 的,但如果你用 optional 关键字显式声明,可以区分“字段没设置”和“字段设了默认值”。我个人建议显式声明,语义更清晰。

3.4 JSON Schema 快速上手

JSON Schema 是纯文本的,可读性最好。同样的用户信息:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "User",
  "type": "object",
  "properties": {
    "userId": { "type": "string" },
    "userName": { "type": "string" },
    "email": { "type": "string" },
    "age": { "type": "integer", "minimum": 0 }
  },
  "required": ["userId", "userName"]
}

JSON Schema 的演化主要靠 allOf、oneOf 等组合关键字。比如你要加一个地址字段,可以定义一个新的 Schema,然后用 allOf 合并。但说实话,JSON Schema 的版本管理不如 Avro 和 Protobuf 那么原生,更多是靠外部工具和约定。

3.5 Schema 版本号管理策略:语义化版本

有了 Schema,怎么管版本?我强烈推荐语义化版本(SemVer)。格式很简单:MAJOR.MINOR.PATCH。

  • MAJOR(主版本):不兼容的变更。比如删除了一个字段,或者改了字段类型。下游必须改造才能适配。
  • MINOR(次版本):向后兼容的新增。比如加了一个可选字段,或者加了一个有默认值的字段。下游不需要改代码。
  • PATCH(补丁版本):向后兼容的修复。比如修正了字段描述,或者调整了校验规则。不影响数据格式。
核心原则: 数据契约的版本号变更,必须与 Schema 的兼容性级别严格对应。MAJOR 对应破坏性变更,MINOR 对应兼容性新增,PATCH 对应无影响修复。

举个例子,你原来有个 User 契约,版本 1.0.0。现在你要加一个 phoneNumber 字段,并且它是可选的(有默认值),那版本号应该升到 1.1.0。如果你把 userId 的类型从 string 改成 int,那就是 2.0.0。

你想想看,如果没有这个规则,下游看到版本号从 1.0.0 变成 1.0.1,以为只是小修小补,结果一接数据发现字段类型变了,那不就炸了吗?

3.6 知识体系总览

下面这张图把本章的核心逻辑串起来了。你可以看到,从 Schema 定义到版本化,再到兼容性检查,是一条完整的链路。

Schema 定义与版本化知识体系 Schema 定义与版本化知识体系 Avro Protobuf JSON Schema Schema 演化规则 兼容性检查 默认值策略 语义化版本(SemVer):MAJOR.MINOR.PATCH MAJOR:破坏性变更 MINOR:兼容性新增 PATCH:无影响修复 核心原则:版本号变更必须与 Schema 兼容性级别严格对应,否则下游会出问题 我建议每次 Schema 变更都走审批流程,并自动检查兼容性

3.7 版本号管理的最佳实践

光有理论不够,落地才是关键。我总结了几条实战经验:

  1. 版本号必须存储在 Schema 元数据中。无论是 Avro 的 doc 字段,还是 Protobuf 的 option,或者 JSON Schema 的 $id,都要把版本号写进去。这样数据流到哪里,版本号就跟到哪里。
  2. 使用 Schema Registry 做兼容性检查。Confluent Schema Registry 支持 Avro 和 Protobuf,可以配置兼容性级别(BACKWARD、FORWARD、FULL 等)。每次注册新版本时,它会自动检查是否兼容。不兼容就拒绝注册。
  3. 版本号变更必须走审批流程。MAJOR 变更需要架构师审批,MINOR 变更需要团队负责人审批,PATCH 变更可以自动通过。这个流程我会在后面的章节详细讲。
  4. 保留所有历史版本。不要只保留最新版本。老版本可能还有数据在流转,你需要能回溯。Schema Registry 天然支持版本历史。
一个小技巧: 我习惯在 Schema 的注释里写上变更原因。比如 Avro 的 doc 字段里写 "v1.1.0: add phoneNumber field for SMS notification"。这样以后排查问题,一眼就能看出每个版本改了啥。