4. Schema 即契约:Avro、Protobuf、JSON Schema 对比与选型

数据契约的核心是什么?说白了,就是一份双方都认的「数据格式说明书」。而这份说明书,在技术层面就体现为 Schema。

我见过太多团队,上来就写代码,接口联调时才发现字段类型对不上、枚举值少了一个、嵌套结构解析失败。嗯,这种坑我踩过不止一次。后来我养成一个习惯:先定 Schema,再写代码。Schema 就是契约的具象化载体。

目前主流的 Schema 方案有三家:Avro、Protobuf、JSON Schema。它们各有各的脾气,选错了后面会很难受。今天我就把这三位的底裤扒一扒,帮你做选型决策。

核心观点:Schema 不是文档,是代码。它应该被版本管理、被校验、被强制遵守。谁绕过 Schema 直接传 JSON,谁就是在给线上事故留后门。

4.1 三剑客的基本画像

先给个全景图,让你心里有个数:

维度 Avro Protobuf JSON Schema
出身 Apache Hadoop 生态 Google IETF 标准(草案)
序列化格式 二进制(紧凑) 二进制(极紧凑) 文本(JSON)
Schema 语言 JSON / IDL .proto 文件 JSON
自描述能力 强(Schema 随数据) 弱(需外部传递) 强(本身就是 JSON)
Schema 演化 强(读写分离) 强(向后兼容) 中(需手动兼容)
性能 高 极高 低
生态 Kafka、Hadoop、Spark gRPC、微服务 REST API、配置文件

这张表你存着,后面每个点我都会展开讲。

4.2 Avro:大数据生态的「老黄牛」

Avro 是我个人在大数据场景下的首选。为什么?因为它天生为「批量数据」和「流式数据」设计。

Avro 有个很骚的特性:Schema 可以嵌入数据文件头。这意味着你拿到一个 .avro 文件,不需要额外加载 Schema 就能直接解析。我在做数据湖项目时,经常需要把历史数据从 HDFS 迁移到对象存储,Avro 这种自描述能力帮了大忙——不用维护一套 Schema Registry,文件本身就是完整的。

Avro Schema 长什么样?

{
  "type": "record",
  "name": "OrderEvent",
  "namespace": "com.example.contract",
  "fields": [
    { "name": "orderId", "type": "string" },
    { "name": "userId", "type": "string" },
    { "name": "amount", "type": "double" },
    { "name": "status", "type": { "type": "enum", "name": "OrderStatus", "symbols": ["CREATED", "PAID", "SHIPPED", "CANCELLED"] } },
    { "name": "items", "type": { "type": "array", "items": "string" } },
    { "name": "createdAt", "type": "long", "logicalType": "timestamp-millis" }
  ]
}

你看,字段类型、枚举、数组、时间戳都定义得很清楚。而且 Avro 支持 logicalType,能把 long 解释成时间戳,这在数据契约里非常实用。

我的经验:Avro 的 Schema 演化规则很严谨。比如你新增一个字段,必须给默认值;删除字段要标记为 deprecated。我曾经因为没给默认值,导致消费端反序列化直接崩了。记住:读 Schema 和写 Schema 可以不同,但必须兼容。

4.3 Protobuf:微服务间的「闪电侠」

Protobuf 是 Google 的亲儿子,性能确实能打。序列化后的体积比 Avro 还小,解析速度也更快。如果你在搞微服务、gRPC,Protobuf 几乎是标配。

但 Protobuf 有个让我头疼的地方:Schema 不随数据走。你拿到一串二进制,如果没有 .proto 文件,那就是天书。所以用 Protobuf 必须搭配 Schema Registry 或者约定好 proto 文件的分发机制。

Protobuf Schema 长什么样?

syntax = "proto3";

package com.example.contract;

message OrderEvent {
  string order_id = 1;
  string user_id = 2;
  double amount = 3;
  enum OrderStatus {
    CREATED = 0;
    PAID = 1;
    SHIPPED = 2;
    CANCELLED = 3;
  }
  OrderStatus status = 4;
  repeated string items = 5;
  int64 created_at = 6;  // timestamp in milliseconds
}

注意看,Protobuf 每个字段都有个 编号(field number)。这个编号是序列化时的 key,一旦发布就不能改。我见过有人为了省事,把字段编号重新排序,结果老数据全解析错了。嗯,这个坑我帮你们踩过了。

避坑指南:Protobuf 的 field number 1-15 只占 1 个字节,16-2047 占 2 个字节。所以频繁使用的字段,编号尽量用 1-15。我曾经接手一个项目,所有字段编号从 100 开始,序列化体积直接翻倍。

4.4 JSON Schema:人类友好的「瑞士军刀」

JSON Schema 跟前两者不是一个赛道。它不追求性能,追求的是可读性和灵活性。你的 API 返回、配置文件、事件消息如果直接用 JSON,那 JSON Schema 就是最自然的契约语言。

但说实话,JSON Schema 的校验性能确实拉胯。我在一个高并发场景下试过,每秒几千次的 JSON Schema 校验,CPU 直接飙到 80%。后来换成了 Protobuf,CPU 降到 10%。所以 JSON Schema 更适合开发期校验和低频接口。

JSON Schema 长什么样?

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "OrderEvent",
  "type": "object",
  "properties": {
    "orderId": { "type": "string" },
    "userId": { "type": "string" },
    "amount": { "type": "number" },
    "status": { "type": "string", "enum": ["CREATED", "PAID", "SHIPPED", "CANCELLED"] },
    "items": { "type": "array", "items": { "type": "string" } },
    "createdAt": { "type": "integer", "format": "utc-millisec" }
  },
  "required": ["orderId", "userId", "amount", "status"]
}

JSON Schema 的优势在于:你不需要额外的序列化/反序列化库。数据本身就是 JSON,Schema 也是 JSON,开发同学看一眼就懂。而且它支持 oneOf、anyOf、if-then-else 这种复杂逻辑,表达能力很强。

我的建议:如果你团队的前端同学比较多,或者需要对外暴露 REST API,JSON Schema 是首选。但记得加上 additionalProperties: false,否则别人给你塞一堆垃圾字段,你的校验形同虚设。

4.5 核心差异:Schema 演化与兼容性

数据契约不是一成不变的。业务在变,字段在加,类型在改。这时候 Schema 的演化能力就至关重要。

我画了一张图,帮你理解三者在演化上的差异:

Schema 演化能力对比 Avro ✅ 读写 Schema 分离 ✅ 新增字段必须有默认值 ✅ 删除字段需标记 deprecated ✅ 支持类型提升(int→long) Protobuf ✅ 字段编号不可变 ✅ 新增字段用 optional ✅ 废弃字段保留编号 ⚠️ 不支持类型变更 JSON Schema ✅ 新增字段默认允许 ✅ 删除字段不影响解析 ⚠️ 类型变更需手动兼容 ⚠️ 无内置演化规则 选型建议 大数据/流处理 → Avro | 微服务/gRPC → Protobuf | REST API/配置 → JSON Schema

4.6 选型决策矩阵

说了这么多,到底怎么选?我整理了一个决策矩阵,你对着自己的场景打勾就行:

场景 推荐方案 理由
Kafka 消息 / 数据管道 Avro Schema 可嵌入消息头,配合 Schema Registry 完美支持演化
微服务间 RPC 调用 Protobuf 性能极致,gRPC 原生支持,跨语言代码生成
对外 REST API JSON Schema 前端直接消费,无需额外序列化,调试方便
配置文件 / 静态数据 JSON Schema 可读性强,支持复杂校验逻辑(如正则、条件)
数据湖 / 数据仓库 Avro 列式存储友好,Hive/Spark 原生支持,文件自描述
移动端 / IoT 设备 Protobuf 序列化体积最小,节省带宽和电量

重要提醒:不要在一个系统里混用多种 Schema 方案。我见过一个团队,Kafka 用 Avro,gRPC 用 Protobuf,REST API 用 JSON Schema,结果数据在不同系统间流转时,需要做三次序列化/反序列化,性能损耗不说,还经常出现字段映射错误。统一一种方案,或者至少统一一种数据模型。

4.7 我的最终建议

如果你问我个人偏好,我会说:

  • 数据基础设施层(Kafka、Hadoop、Spark):用 Avro。它的 Schema 演化规则最成熟,生态最完善。
  • 服务间通信层(gRPC、微服务):用 Protobuf。性能就是王道,而且代码生成能省很多事。
  • 人机交互层(API、配置、调试):用 JSON Schema。别让前端同学去解析二进制,那是给自己找麻烦。

但不管选哪个,记住一句话:Schema 就是契约,契约就是代码。把它纳入 CI/CD 流程,每次变更都要走评审、都要做兼容性检查。只有这样,你的数据契约体系才能真正落地。