4、Avro Schema实战:定义你的第一个Avro Schema,兼容性规则详解
好,咱们开始动手写Schema。前面讲了那么多理论,今天就来点实在的。我会带你从零定义一个Avro Schema,然后把那四个兼容性规则——BACKWARD、FORWARD、FULL、NONE——掰开揉碎讲清楚。
说实话,我见过太多团队在Schema兼容性上栽跟头。有的是上线前一天才发现新Schema把老数据全搞崩了,有的是为了兼容性把Schema写得跟天书一样。嗯,咱们今天就把这些坑填平。
4.1 定义你的第一个Avro Schema
Avro Schema有两种写法:JSON格式和IDL(接口定义语言)。我个人习惯用JSON,因为它在Kafka、Hadoop这些生态里是原生支持的。咱们先看一个最简单的例子。
{
"namespace": "com.example.user",
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"},
{"name": "email", "type": "string"}
]
}
这个Schema定义了一个叫User的记录,有三个字段。你想想看,它其实就是一个数据契约——告诉所有生产者和消费者:数据长这样,别乱来。
com.公司名.部门名的格式。我在项目中见过两个团队都定义了Order,结果序列化时串了,排查了半天。
咱们再加点料。实际业务中,字段往往有默认值,或者允许为空。看这个升级版:
{
"namespace": "com.example.user",
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string", "default": ""},
{"name": "email", "type": ["null", "string"], "default": null},
{"name": "age", "type": "int", "default": 0}
]
}
这里有几个关键点:
- default:给字段一个默认值。当读取旧数据时,如果新Schema多了字段,就用默认值填充。
- 联合类型:
["null", "string"]表示这个字段可以是null或者string。注意,null要写在第一位,这是Avro的约定。
为什么要加默认值?说白了,就是为了兼容性。没有默认值,新Schema读旧数据就会直接报错。我曾经在一个支付系统里,就因为忘了给新加的discount字段设默认值,导致凌晨的批处理任务全挂了……嗯,从那以后我再也不敢漏掉default了。
4.2 兼容性规则:四个选项,四种策略
Avro的兼容性规则,说白了就是控制Schema怎么变、变多大。咱们用一张图来理解:
这张图把四个规则的关系讲清楚了。咱们一个一个来看。
4.3 BACKWARD 兼容:新Schema能读旧数据
这是最常用的规则。场景是这样的:你先用旧Schema写了一批数据,然后升级了Schema。BACKWARD保证新Schema能正常读取旧数据。
怎么做到?说白了就是:新Schema只能删字段,或者加有默认值的字段。不能加必填字段。
看个例子:
// 旧Schema
{
"type": "record",
"name": "Order",
"fields": [
{"name": "id", "type": "int"},
{"name": "amount", "type": "double"}
]
}
// 新Schema(BACKWARD兼容)
{
"type": "record",
"name": "Order",
"fields": [
{"name": "id", "type": "int"},
{"name": "amount", "type": "double"},
{"name": "discount", "type": "double", "default": 0.0}
]
}
新Schema加了discount字段,但有默认值0.0。读旧数据时,没有discount字段,就用0.0填充。完美。
4.4 FORWARD 兼容:旧Schema能读新数据
这个规则正好反过来。场景是:消费者还没升级Schema,但生产者已经用新Schema写数据了。FORWARD保证旧Schema能正常读取新数据。
怎么做?旧Schema只能忽略新加的字段,或者给新字段设默认值。但注意,这里有个关键点:旧Schema必须为所有它不认识的字段提供默认值处理机制。
Avro的FORWARD兼容其实是通过字段ID和默认值实现的。每个字段在Schema里都有一个隐式的ID(按顺序排列),新加的字段ID更大。旧Schema读到不认识的ID时,就跳过。
看例子:
// 旧Schema
{
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "int"}
]
}
// 新Schema(FORWARD兼容)
{
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string", "default": ""}
]
}
旧Schema只有id字段。读到新数据时,遇到name字段,它不认识,就跳过。但注意,如果新Schema的name没有默认值,旧Schema读到这条数据时,因为缺少name字段,就会报错。所以FORWARD兼容要求:新加的字段必须有默认值。
我建议你在设计Schema时,尽量给每个字段都设默认值。哪怕默认值是null或者空字符串,也比没有强。这样以后改起来灵活得多。
4.5 FULL 兼容:双向兼容
FULL就是BACKWARD + FORWARD。新Schema能读旧数据,旧Schema也能读新数据。说白了,就是两边都能互相理解。
这要求很严格:
- 只能加有默认值的字段
- 只能删有默认值的字段
- 不能改字段类型
- 不能改字段名
看个例子:
// Schema V1
{
"type": "record",
"name": "Product",
"fields": [
{"name": "id", "type": "int"},
{"name": "price", "type": "double", "default": 0.0}
]
}
// Schema V2(FULL兼容)
{
"type": "record",
"name": "Product",
"fields": [
{"name": "id", "type": "int"},
{"name": "price", "type": "double", "default": 0.0},
{"name": "stock", "type": "int", "default": 0}
]
}
V2加了stock字段,有默认值。V1读V2数据时,忽略stock;V2读V1数据时,stock用默认值0填充。双向都通。
4.6 NONE 兼容:没有规则就是规则
NONE就是没有兼容性要求。你想怎么改就怎么改,加字段、删字段、改类型,随便来。
但代价是什么?所有用旧Schema的消费者,在读到新数据时都会报错。除非你同时升级所有生产者和消费者。
什么时候用NONE?
- 开发环境,数据不重要
- 一次性批处理,跑完就删
- 所有客户端都能同时升级(比如内部微服务,用同一个发布流水线)
我个人几乎不用NONE。哪怕在开发环境,我也习惯用BACKWARD。因为有一次我在测试环境用了NONE,结果忘了通知下游团队,他们消费数据时直接崩了……嗯,从那以后我就长记性了。
4.7 四种规则对比
咱们用一张表总结一下:
| 规则 | 新Schema读旧数据 | 旧Schema读新数据 | 允许操作 | 推荐场景 |
|---|---|---|---|---|
| BACKWARD | ✅ 支持 | ❌ 不支持 | 加有默认值的字段、删字段 | 消费者先升级,生产者后升级 |
| FORWARD | ❌ 不支持 | ✅ 支持 | 加有默认值的字段 | 生产者先升级,消费者后升级 |
| FULL | ✅ 支持 | ✅ 支持 | 加有默认值的字段、删有默认值的字段 | 生产者和消费者独立升级 |
| NONE | ❌ 不支持 | ❌ 不支持 | 任意修改 | 开发环境、一次性任务 |
你想想看,选哪个规则其实取决于你的发布策略。如果你们是蓝绿部署,消费者先切流量,那就用BACKWARD。如果是灰度发布,生产者先写新数据,那就用FORWARD。如果两边独立迭代,那就用FULL。
4.8 避坑指南
最后,分享几个我踩过的坑:
- 默认值不是万能药:加了默认值,旧数据读出来就是默认值。但业务逻辑可能不认这个默认值。比如
discount默认0.0,但业务上0.0表示没有折扣,而实际旧数据可能有折扣只是没存……嗯,这就要靠业务对齐了。 - 枚举类型要小心:Avro的enum类型,加新值默认是FORWARD不兼容的。除非你给enum设默认值。但说实话,我建议少用enum,用string加校验代替。
- 字段改名是大忌:Avro没有字段别名机制。改名相当于删一个字段再加一个字段,兼容性很难保证。我一般用
@deprecated注解标记旧字段,等所有消费者都迁移了再删。 - 测试!测试!测试!:每次改Schema,一定要用兼容性检查工具跑一遍。Avro官方提供了
SchemaCompatibility类,可以写单元测试。别偷懒,我见过太多上线前才发现不兼容的案例了。
好了,这一章的内容就到这。你先把第一个Schema写出来,跑通序列化和反序列化,然后试着改一改字段,看看不同兼容性规则的效果。动手才是最好的学习方式。