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 兼容性检查的核心流程。这张图我画了好几次才满意,希望能帮你理解。

Avro Schema 兼容性检查流程 Reader Schema Writer Schema 检查引擎 兼容? COMPATIBLE INCOMPATIBLE 检查项明细 1. 字段是否存在(新增字段必须有默认值) 2. 字段类型是否兼容(int → long 可以,int → string 不行) 3. 默认值是否匹配(union 类型必须有默认值)

检查引擎会逐字段对比。核心逻辑就三条:

  • 字段存在性:新 Schema 删字段?不行。新 Schema 加字段?必须有默认值。
  • 字段类型兼容:比如 int 可以升 long,但 string 不能降 int。
  • 默认值匹配:union 类型必须给默认值,否则反序列化时不知道用哪个类型。

4.6 避坑指南

最后,分享几个我踩过的坑:

  • 命名空间不一致:两个 Schema 的 namespace 不同,Avro 会认为它们是不同的类型,兼容性检查直接失败。我建议团队统一命名规范,比如都用 com.company.project。
  • 枚举类型改动:枚举加新值,默认情况下是向前兼容的,但不是向后兼容。因为老代码不认识新值。如果你要加枚举值,记得用 default 属性。
  • 字段顺序:Avro 依赖字段顺序进行序列化。如果你调整了字段顺序,即使类型没变,也可能导致反序列化错乱。所以,永远不要改变已有字段的顺序。

我的建议:在 CI/CD 流程中加入 Schema 兼容性检查。每次提交 Schema 变更时,自动跑一遍检查,不通过就不让合并。这样能避免很多线上事故。

好了,Avro 的兼容性实战就聊到这儿。代码示例和检查逻辑都给你了,剩下的就是动手试试。记住,理论再清楚,不如自己跑一遍代码来得实在。