Schema 定义语言:Avro、Protobuf、JSON Schema 的对比与选择

数据契约的核心,说白了就是一份双方都认的「数据结构说明书」。但用什么语言来写这份说明书?这就涉及到我们今天要聊的 Schema 定义语言。

我在过去十年里,几乎把这三种主流方案都用了个遍。从 Hadoop 时代的 Avro,到微服务标配的 Protobuf,再到 API 网关常用的 JSON Schema。每种都有它的脾气,也各有它的主场。

你想想看,选错了 Schema 语言,就像用写散文的笔法去写法律合同——不是不行,但后面改起来真要命。咱们今天就把它们掰开揉碎,看看各自到底适合什么场景。

三足鼎立的局面

先看一张总览图,帮你快速建立全局认知。

Schema 定义语言三足鼎立 Apache Avro JSON 格式定义 动态类型 自带 IDL Schema 演化强 适合:大数据 / Kafka 序列化:紧凑二进制 Protocol Buffers .proto 文件定义 强类型 + 代码生成 向后兼容优秀 跨语言支持好 适合:微服务 / gRPC 序列化:极紧凑二进制 JSON Schema 纯 JSON 定义 自描述 校验能力强 人类可读 适合:API / 配置文件 序列化:文本 JSON 选择标准:团队技术栈 × 性能要求 × 跨语言需求 × Schema 演化频率 没有银弹,只有最适合你当前场景的方案

Apache Avro:大数据的「老伙计」

Avro 是 Hadoop 生态的亲儿子。我最早接触它是在做离线数仓的时候,那时候 Hive 表的结构定义全靠它。

它的核心特点有两个:Schema 用 JSON 写,数据用二进制存。这意味着什么?你既可以用文本编辑器直接改 Schema,又能享受紧凑的序列化体积。

Avro 的 Schema 长什么样?

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

注意看 email 字段,它用了联合类型 ["null", "string"],还给了默认值。这就是 Avro 的演化机制——新加的字段只要有默认值,老数据就能无缝兼容。

我的经验:在 Kafka 消息队列里,Avro 配合 Schema Registry 简直是绝配。生产者只发二进制数据,消费者从 Registry 拉 Schema 来解析。我曾经帮一个团队把 Kafka 消息体积降了 70%,就是靠 Avro 替换了 JSON 序列化。

Avro 的演化规则

操作 是否兼容 说明
新增字段(有默认值) ✅ 向前兼容 老数据读新 Schema 没问题
删除字段 ⚠️ 需谨慎 老数据可能丢失信息
修改字段类型 ❌ 不兼容 除非是 widening
重命名字段 ❌ 不兼容 需要 alias 机制

Protocol Buffers:微服务的「硬通货」

Protobuf 是 Google 出品,现在几乎成了微服务间通信的标配。尤其是搭配 gRPC,那体验确实丝滑。

我个人习惯用 Protobuf 的场景是:跨语言服务调用。你想想看,一个系统里 Java 写后端,Go 写中间层,Python 做数据科学——怎么让它们愉快地交换数据?Protobuf 的代码生成器一跑,各语言的结构体就齐活了。

Protobuf 的 .proto 文件

syntax = "proto3";

package com.example;

message User {
  string name = 1;
  int32 age = 2;
  optional string email = 3;
}

注意每个字段后面的数字——那是字段编号,不是默认值。Protobuf 用编号来标识字段,所以一旦发布,编号就不能改。我曾经见过有人把字段 1 和字段 2 调换顺序,结果线上数据全乱了……嗯,那是个惨痛的教训。

避坑指南:Protobuf 的字段编号 1-15 只占 1 个字节,16-2047 占 2 个字节。所以高频字段尽量用小编号。我曾经优化过一个接口,把最常用的 5 个字段挪到 1-5 号,序列化体积直接降了 30%。

Protobuf 的演化哲学

Protobuf 的设计原则是:永远不要破坏向后兼容。具体来说:

  • 可以新增字段,但必须是 optional 或 repeated
  • 可以删除字段,但要保留编号(用 reserved 关键字标记)
  • 不能修改字段类型
  • 不能修改字段编号

为什么这么严格?因为 Protobuf 的二进制格式极度紧凑,它依赖编号来定位字段。一旦编号乱了,解析器就彻底懵了。

JSON Schema:API 世界的「通用语」

JSON Schema 跟前两者完全不同。它不关心序列化,只关心校验。说白了,它就是一份「数据长什么样的说明书」。

我最早用 JSON Schema 是在做 RESTful API 的时候。后端返回的数据对不对?前端传的参数全不全?用 JSON Schema 一验就知道。

JSON Schema 示例

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "age": {
      "type": "integer",
      "minimum": 0,
      "maximum": 150
    },
    "email": {
      "type": "string",
      "format": "email"
    }
  },
  "required": ["name", "email"]
}

你看,它不光定义了类型,还能约束长度、范围、格式。这种表达能力是 Avro 和 Protobuf 不具备的。

我的建议:如果你在做 OpenAPI 规范(Swagger),JSON Schema 几乎是必选项。它天然跟 HTTP 协议契合,而且人类可读。我见过很多团队用 JSON Schema 做 API 网关的请求校验,效果非常好。

怎么选?一张表说清楚

维度 Avro Protobuf JSON Schema
定义格式 JSON / IDL .proto 文件 纯 JSON
序列化格式 二进制 二进制 文本 JSON
序列化速度 快 极快 慢
序列化体积 小 极小 大
Schema 演化 强(自带演化机制) 强(需遵守规则) 弱(无内置演化)
代码生成 支持 原生支持 需第三方工具
人类可读 中等(JSON 部分可读) 低(需工具解析) 高(纯文本)
校验能力 弱 弱 强(格式、范围等)
最佳场景 大数据 / Kafka 微服务 / gRPC API 文档 / 配置文件

我的选择框架

说了这么多,到底怎么选?我总结了一个简单的决策流程:

  1. 先看传输场景:如果是消息队列或大数据管道,优先 Avro
  2. 再看跨语言需求:如果多语言服务互相调用,优先 Protobuf
  3. 最后看校验需求:如果是 REST API 或配置文件,优先 JSON Schema

当然,实际项目中经常混用。比如我最近的一个项目:Kafka 消息用 Avro,服务间 RPC 用 Protobuf,API 文档用 JSON Schema。三者各司其职,互不冲突。

核心原则:Schema 定义语言只是工具,数据契约的核心是「约定」本身。选一个团队最熟悉、生态最完善、维护成本最低的方案,比追求「最好」的方案重要得多。