3. Schema Registry与数据契约

在机器学习数据平台中,特征数据往往由上游特征工程任务写入 Kafka,再由下游训练、推理、监控等多个消费者读取。如果生产者悄悄把 user_age 从 int 改成 string,或者删掉一个特征字段,下游消费者可能在反序列化时直接抛异常,也可能静默读到错误值,最终污染训练样本。Schema Registry 的核心价值,就是把“消息长什么样”从口头约定变成可注册、可校验、可演化的数据契约。

数据契约与 Schema Registry 的角色

数据契约描述的是主题中消息的字段名、类型、默认值、是否必填以及版本关系。Confluent Schema Registry 作为独立服务,为每个 subject(通常对应 <topic>-value 或 <topic>-key)保存 schema 版本,并给每个版本分配全局唯一的 schema ID。生产者发送消息时,序列化器把 schema ID 写入消息头部,消息体只携带按该 schema 编码的二进制数据;消费者拉取消息后,从消息头读取 schema ID,向 Registry 获取对应 schema,再完成反序列化。

这种设计带来两个直接收益:一是消息体不必重复携带完整 schema,节省带宽;二是消费者拿到的 schema 与生产者写入时完全一致,避免“按错误结构解析”的问题。对于 ML 特征数据,这意味着训练侧和推理侧可以基于同一份契约读取特征,减少线上线下特征不一致。

Schema Registry 数据契约流转与兼容性校验 生产者 特征工程任务 Avro / Protobuf JSON Schema Schema Registry subject: ml-features-value 版本 v1 / v2 / v3 兼容性策略校验 schema ID 分配 消费者 训练 / 推理 / 监控 按 schema ID 反序列化 特征一致性校验 注册 / 获取 schema schema ID + 数据 兼容性策略与演化方向 BACKWARD 新 schema 可读旧数据 允许:新增带默认值字段 允许:删除字段 默认策略,消费者先升级 ML 特征常用 FORWARD 旧 schema 可读新数据 允许:删除带默认值字段 允许:新增字段 生产者先升级 适合灰度发布 FULL 双向兼容 新增/删除均需默认值 升级顺序不敏感 最严格,适合核心特征 推荐用于关键主题 NONE 不做兼容性检查 任意变更均可注册 风险最高 仅用于临时实验 生产环境禁用

三种 Schema 格式的取舍

Avro 是 Kafka 生态中最常用的格式。它使用 JSON 定义 schema,支持丰富的基本类型和复杂类型(record、array、map、union),并且对默认值支持完善,非常适合需要频繁演化的特征表。Avro 的二进制编码紧凑,字段按顺序写入,因此新增字段必须带默认值,否则旧消费者无法解析。

Protobuf 使用 .proto 文件定义消息,字段通过 tag 编号标识,而不是按顺序。这意味着新增字段只要使用新编号,旧消费者会忽略未知字段,天然具备较好的前向兼容性。Protobuf 适合跨语言、跨团队的特征服务,尤其是推理侧使用 Java、Go、Python 混合部署的场景。但 Protobuf 的默认值语义与 Avro 不同,删除字段需要保留编号并标记 reserved,否则后续复用编号会导致解析错乱。

JSON Schema 以人类可读的 JSON 描述结构,适合调试、日志和低频特征。它的优势是无需代码生成,消费者可以直接用 JSON 解析;缺点是消息体较大,且 JSON 本身没有强制的字段顺序和类型约束,兼容性判断更依赖 Registry 的策略实现。对于高吞吐的实时特征流,通常不把 JSON Schema 作为首选。

注册、演化与兼容性校验的实际操作

假设我们有一个主题 ml-features,初始 schema 为 Avro 格式,包含 user_id、age、city 三个字段。生产者启动时,序列化器会尝试向 Registry 注册该 schema。如果 subject 已存在且新 schema 与当前版本兼容,注册成功并返回 schema ID;如果不兼容,注册失败,生产者会收到 SchemaRegistryException,消息无法发送。

# 查看 subject 下所有版本
curl -s http://schema-registry:8081/subjects/ml-features-value/versions

# 获取最新版本 schema
curl -s http://schema-registry:8081/subjects/ml-features-value/versions/latest

# 检查新 schema 是否兼容(不注册)
curl -s -X POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \
  --data '{"schema": "{\"type\":\"record\",\"name\":\"MLFeature\",\"fields\":[{\"name\":\"user_id\",\"type\":\"string\"},{\"name\":\"age\",\"type\":\"int\"},{\"name\":\"city\",\"type\":\"string\"},{\"name\":\"score\",\"type\":\"double\",\"default\":0.0}]}"}' \
  http://schema-registry:8081/compatibility/subjects/ml-features-value/versions/latest

如果返回 {"is_compatible":true},说明新增带默认值的 score 字段符合 BACKWARD 策略,可以正式注册。若把 age 的类型从 int 改成 string,兼容性检查会返回 false,因为旧数据中的整数无法被新 schema 正确读取。此时应改用新增字段的方式,而不是修改已有字段类型。

在 ML 特征场景中,建议对核心特征主题设置 FULL 兼容性策略,对实验性特征主题设置 BACKWARD。任何删除字段或修改类型之前,先通过兼容性检查接口验证,再走变更评审。

保障 ML 特征一致性的关键实践

第一,为每个特征主题明确 subject 命名规范,例如 <domain>-<entity>-features-value,避免不同团队共用主题导致 schema 冲突。第二,在 CI 流程中加入 schema 兼容性检查,把 compatibility 接口作为流水线门禁,防止不兼容变更进入生产。第三,消费者端不要硬编码 schema,而是通过 Registry 动态获取,确保训练和推理使用同一版本。第四,对特征字段的默认值要谨慎:默认值会直接影响缺失特征的填充结果,必须与特征工程文档保持一致。

当特征 schema 发生演化时,建议同时记录变更原因、影响的下游消费者和回滚方案。Schema Registry 保存了版本历史,但业务语义需要人工维护。只有把技术契约和业务语义一起管理,才能真正保障 ML 特征数据在生产者、训练任务和推理服务之间的一致性。