4、Avro Schema兼容性实战
好,咱们进入实战环节。前面聊了那么多理论,今天咱们就动手,看看 Avro Schema 到底怎么定义,四种兼容性模式怎么用,以及怎么用代码做检查。
我个人觉得,Avro 的 Schema 定义是这几个序列化框架里最直观的。它用 JSON 格式,读起来不费劲。但你别看它简单,坑也不少。我刚开始用的时候,就因为在 Schema 里写错了一个字段类型,导致线上数据全写不进去,排查了半天。
4.1 Avro Schema 定义基础
Avro Schema 支持基本类型和复杂类型。基本类型包括:null、boolean、int、long、float、double、bytes、string。复杂类型有 record、enum、array、map、union、fixed。
咱们最常用的是 record。一个典型的 Schema 长这样:
{
"namespace": "com.example",
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"},
{"name": "email", "type": ["null", "string"], "default": null}
]
}
注意看 email 字段。它用了 union 类型 ["null", "string"],并且给了默认值 null。为什么要这么写?
嗯,这里有个关键点:如果你想让字段可选,必须用 union 类型,并且提供默认值。否则,在向后兼容检查时,新 Schema 加了字段,老数据读不了,直接报错。我在项目中见过太多人忽略这个细节了。
4.2 四种兼容性模式详解
Avro 定义了四种兼容性模式。说白了,就是控制 Schema 演化时,新 Schema 能不能读老数据,或者老 Schema 能不能读新数据。
| 模式 | 含义 | 新读老 | 老读新 |
|---|---|---|---|
| BACKWARD | 向后兼容 | ✅ 可以 | ❌ 不行 |
| FORWARD | 向前兼容 | ❌ 不行 | ✅ 可以 |
| FULL | 完全兼容 | ✅ 可以 | ✅ 可以 |
| NONE | 不检查 | ❌ 不行 | ❌ 不行 |
你想想看,这四种模式对应了不同的业务场景。比如你的消费者升级慢,生产者升级快,那就用 BACKWARD。反过来,消费者先升级,生产者后升级,就用 FORWARD。两边同时升级?那用 FULL 最省心。
核心原则:BACKWARD 保证新 Schema 能读老数据;FORWARD 保证老 Schema 能读新数据;FULL 两者都保证;NONE 什么都不保证。
4.3 兼容性检查实战:Java 实现
咱们先看 Java 怎么搞。Avro 官方提供了 SchemaCompatibility 工具类,用起来很简单。
import org.apache.avro.Schema;
import org.apache.avro.SchemaCompatibility;
import org.apache.avro.SchemaCompatibility.SchemaCompatibilityType;
public class AvroCompatibilityChecker {
public static void main(String[] args) {
String readerSchemaStr = "{"
+ "\"type\": \"record\","
+ "\"name\": \"User\","
+ "\"fields\": ["
+ " {\"name\": \"id\", \"type\": \"int\"},"
+ " {\"name\": \"name\", \"type\": \"string\"}"
+ "]}";
String writerSchemaStr = "{"
+ "\"type\": \"record\","
+ "\"name\": \"User\","
+ "\"fields\": ["
+ " {\"name\": \"id\", \"type\": \"int\"},"
+ " {\"name\": \"name\", \"type\": \"string\"},"
+ " {\"name\": \"email\", \"type\": [\"null\", \"string\"], \"default\": null}"
+ "]}";
Schema readerSchema = new Schema.Parser().parse(readerSchemaStr);
Schema writerSchema = new Schema.Parser().parse(writerSchemaStr);
SchemaCompatibilityType result = SchemaCompatibility.checkReaderWriterCompatibility(
readerSchema, writerSchema).getType();
System.out.println("兼容性检查结果: " + result);
// 输出: COMPATIBLE
}
}
这里要注意:checkReaderWriterCompatibility 的第一个参数是 reader Schema,第二个是 writer Schema。顺序搞反了,结果就错了。我曾经在代码 review 时看到同事把参数传反了,导致线上误报兼容性问题,数据链路直接断了。
小技巧:你可以把 reader Schema 理解为「消费者手里的 Schema」,writer Schema 理解为「生产者写数据时用的 Schema」。这样记不容易搞混。
4.4 兼容性检查实战:Python 实现
Python 这边,我们用 avro 库。它没有 Java 那么方便的检查工具,但我们可以自己写一个。
import avro.schema
from avro.schema import SchemaCompatibility
reader_schema = avro.schema.parse("""
{
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"}
]
}
""")
writer_schema = avro.schema.parse("""
{
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"},
{"name": "email", "type": ["null", "string"], "default": null}
]
}
""")
result = SchemaCompatibility.check_reader_writer_compatibility(
reader_schema, writer_schema
)
print(f"兼容性检查结果: {result}")
# 输出: True
Python 的 check_reader_writer_compatibility 返回布尔值。如果返回 True,说明兼容;False 就是不兼容。逻辑和 Java 一样,reader 在前,writer 在后。
注意:Python 的 avro 库版本较老,对某些复杂 Schema 的兼容性检查可能不够严格。如果你在生产环境用,建议还是以 Java 的检查结果为准。我遇到过 Python 检查通过,但 Java 检查报错的情况。
4.5 兼容性检查的核心逻辑
咱们用一张图来梳理一下 Avro 兼容性检查的核心流程。这张图我画了好几次才满意,希望能帮你理解。
检查引擎会逐字段对比。核心逻辑就三条:
- 字段存在性:新 Schema 删字段?不行。新 Schema 加字段?必须有默认值。
- 字段类型兼容:比如
int可以升long,但string不能降int。 - 默认值匹配:union 类型必须给默认值,否则反序列化时不知道用哪个类型。
4.6 避坑指南
最后,分享几个我踩过的坑:
- 命名空间不一致:两个 Schema 的
namespace不同,Avro 会认为它们是不同的类型,兼容性检查直接失败。我建议团队统一命名规范,比如都用com.company.project。 - 枚举类型改动:枚举加新值,默认情况下是向前兼容的,但不是向后兼容。因为老代码不认识新值。如果你要加枚举值,记得用
default属性。 - 字段顺序:Avro 依赖字段顺序进行序列化。如果你调整了字段顺序,即使类型没变,也可能导致反序列化错乱。所以,永远不要改变已有字段的顺序。
我的建议:在 CI/CD 流程中加入 Schema 兼容性检查。每次提交 Schema 变更时,自动跑一遍检查,不通过就不让合并。这样能避免很多线上事故。
好了,Avro 的兼容性实战就聊到这儿。代码示例和检查逻辑都给你了,剩下的就是动手试试。记住,理论再清楚,不如自己跑一遍代码来得实在。