4. Schema 兼容性类型:Backward、Forward、Full、None 兼容性详解

兼容性策略,说白了就是 Schema Registry 用来判断「新 Schema 能不能替换旧 Schema」的规则。我刚开始接触时也觉得这玩意儿有点抽象,但后来在线上踩过几次坑,才真正理解它的重要性。

你想想看,Kafka 消息的生产者和消费者往往是独立部署的。生产者升级了 Schema,消费者还没升级,怎么办?或者反过来?兼容性策略就是用来解决这个问题的。

Schema Registry 支持四种兼容性类型:Backward、Forward、Full 和 None。下面我一个一个拆开讲。

4.1 Backward 兼容性(向后兼容)

这是最常用的一种策略。它的规则很简单:新 Schema 必须能读取旧 Schema 写的数据。

换句话说,消费者先升级,生产者后升级。消费者用新 Schema 去读旧数据,不能报错。

核心原则:使用新 Schema 的消费者,必须能处理使用旧 Schema 写入的消息。

举个例子,旧 Schema 有个字段 name,新 Schema 加了一个可选字段 age(带默认值)。那消费者用新 Schema 读旧数据时,age 字段取默认值,没问题。这就符合 Backward 兼容。

但如果新 Schema 删掉了 name 字段,或者把 name 的类型从 string 改成了 int,那消费者读旧数据就会崩溃。这种变更就不允许。

我的习惯:在微服务架构中,如果消费者数量多、升级周期长,我一般优先选 Backward 兼容。这样可以先升级所有消费者,再慢慢升级生产者,风险可控。

4.2 Forward 兼容性(向前兼容)

Forward 兼容和 Backward 正好相反:旧 Schema 必须能读取新 Schema 写的数据。

也就是说,生产者先升级,消费者后升级。消费者用旧 Schema 去读新数据,不能报错。

核心原则:使用旧 Schema 的消费者,必须能处理使用新 Schema 写入的消息。

举个例子,旧 Schema 有 name 和 age 两个字段,新 Schema 删掉了 age。那消费者用旧 Schema 读新数据时,发现 age 字段缺失,如果 Schema Registry 允许字段缺失(默认值或 nullable),那就能正常工作。

我曾经在一个实时数仓项目中用过 Forward 兼容。数据源端(生产者)经常要加新字段,但下游的 Flink 任务(消费者)升级比较慢。用 Forward 兼容,生产者可以随时加字段,消费者那边只要忽略不认识的字段就行。

注意:Forward 兼容对字段删除比较严格。旧 Schema 有的字段,新 Schema 不能随便删,除非该字段有默认值或是 optional 的。

4.3 Full 兼容性(完全兼容)

Full 兼容是 Backward 和 Forward 的叠加:新 Schema 必须能读旧数据,旧 Schema 也必须能读新数据。

说白了,就是双向兼容。生产者和消费者可以任意顺序升级,谁先谁后都行。

核心原则:新 Schema 和旧 Schema 必须能互相读取对方写入的数据。

这种策略最安全,但限制也最多。比如你只能添加带默认值的可选字段,不能删除字段,不能修改字段类型。任何可能破坏双向兼容的变更都会被拒绝。

我记得有一次,团队里有人想给一个已有字段加个非空约束,结果被 Full 兼容策略拦住了。当时大家还觉得烦,后来想想,如果真改了,线上那些还没升级的消费者肯定要出问题。

兼容性类型 生产者先升级 消费者先升级 适用场景
Backward ❌ 不允许 ✅ 允许 消费者多,升级周期长
Forward ✅ 允许 ❌ 不允许 生产者频繁变更,消费者升级慢
Full ✅ 允许 ✅ 允许 生产者和消费者升级节奏不确定
None ✅ 允许 ✅ 允许 开发/测试环境,或完全可控的场景

4.4 None 兼容性(不兼容)

None 兼容性,顾名思义,就是不做任何兼容性检查。新 Schema 可以直接覆盖旧 Schema,不管字段怎么变。

这个策略风险最大,但灵活性也最高。我一般只在以下场景用:

  • 开发环境:Schema 还在频繁迭代,没必要做兼容性检查
  • 一次性任务:数据用完就删,不需要考虑历史数据
  • 完全可控的系统:所有生产者和消费者都在同一个部署单元里,可以同时升级

避坑指南:我曾经在生产环境误用过 None 兼容性,结果新 Schema 删了一个字段,导致下游消费者反序列化失败,线上报警一片。从那以后,我严格规定生产环境至少用 Backward 兼容。

4.5 兼容性检查的核心逻辑

不管哪种策略,Schema Registry 的检查逻辑都基于一个核心概念:Schema 的演化图。

每次注册新 Schema 时,Registry 会拿它和当前最新的 Schema 做对比。对比的依据是 Avro/Protobuf/JSON Schema 的兼容性规则,比如:

  • 字段是否被删除
  • 字段类型是否被修改
  • 新增字段是否有默认值
  • 默认值是否兼容

下面这张图展示了四种兼容性策略的决策流程:

Schema 兼容性检查决策流程 注册新 Schema 判断兼容性类型 Backward Forward Full None 新 Schema 能读旧数据? 旧 Schema 能读新数据? 双向都能读? 不检查,直接通过 通过 ✅ 通过 ✅ 通过 ✅ 通过 ✅

4.6 如何选择兼容性策略

没有银弹。选择哪种策略,取决于你的业务场景和团队节奏。

我个人总结了几条经验:

  • Backward:适合消费者多、升级周期长的场景。比如你有一个数据平台,下游有几十个数据应用,你不可能让它们同时升级。
  • Forward:适合生产者频繁变更的场景。比如数据源端经常要加字段,下游消费者只需要忽略不认识的字段就行。
  • Full:适合生产者和消费者升级节奏不确定的场景。虽然限制多,但最安全。
  • None:只用在开发环境或一次性任务中。生产环境慎用,慎用,再慎用。

我的建议:如果你刚开始用 Schema Registry,先从 Backward 兼容开始。等团队对 Schema 演化有了感觉,再根据实际情况调整。别一上来就上 Full,限制太多容易劝退团队。

4.7 兼容性检查的代码示例

最后,给一个实际操作的例子。假设我们用 Avro Schema,通过 REST API 注册新 Schema 时,可以指定兼容性策略:

// 注册新 Schema 时指定兼容性策略
POST /subjects/my-topic-value/versions
Content-Type: application/vnd.schemaregistry.v1+json

{
  "schema": "{\"type\":\"record\",\"name\":\"User\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"age\",\"type\":\"int\",\"default\":0}]}",
  "compatibility": "BACKWARD"
}

如果新 Schema 不满足 Backward 兼容,Registry 会返回 409 Conflict,并告诉你哪里不兼容。

嗯,兼容性策略就讲到这里。理解了这四种类型,你就能根据实际场景做出合适的选择了。