Skip to content

消息 Schema 演进与兼容 ​

难度与前置

难度:进阶
前置:生产或消费过带结构的消息;知道加字段可能弄挂下游。
读完能做什么:制定兼容策略(向前/向后/全兼容),用 Schema Registry 或约定约束演进,并用兼容性检查与回放验收。

适用场景 ​

  • Topic 已有多消费者(实时作业、入湖、审计),需加字段/改类型
  • 引入 Avro/Protobuf + Schema Registry
  • 数仓 ODS 贴源半结构化,频繁「多一个字段」

不适用:

  • 单生产者单消费者且可同步发版停机切换(仍建议留版本号)
  • 纯日志字符串无结构约定、且明确不解析字段
  • 与 分区设计 无关的容量问题(分开治)

真实场景(一条) ​

点击 Topic 原 JSON 无 schema 约束。某次客户端把 price 从数字改成字符串,Flink 作业反序列化失败,重启循环,Lag 飙升。改造:上 Avro + Registry,兼容级别 BACKWARD;CI 对 schema 做兼容性门禁;消费者对未知字段忽略、对缺失字段用默认值。此后加可选字段可灰度,破坏性变更走新 Topic / 新版本策略。

怎么做(步骤清单) ​

1. 先选序列化与「真相源」 ​

格式优点注意
JSON易读无强约束,易 silently 变坏
AvroSchema + 默认值友好需 Registry 或内嵌
Protobuf跨语言、演进清晰字段号纪律
混合ODS 先 JSON尽早收缩到强 schema

原则:契约进 Registry(或仓库中的 .avsc/.proto),不要只活在 Wiki。

2. 兼容级别(务必统一语言) ​

级别含义(常见 Registry 语义)典型用途
BACKWARD新 schema 读旧数据先升级消费者再升级生产者
FORWARD旧 schema 读新数据先升级生产者
FULL双向多消费者多生产者最稳
加 _TRANSITIVE对历史所有版本长期多版本并存
text
多数数据总线默认建议:FULL 或 FULL_TRANSITIVE
若做不到:明确「升级顺序」写进发布清单

3. 安全演进 vs 破坏性变更 ​

通常安全(视级别与默认值):

  • 增加可选字段(带 default)
  • 删除字段(在 BACKWARD 下需谨慎,看谁读谁写)
  • 扩宽枚举(注意未知枚举处理)

通常破坏:

  • 改字段类型(int → string)
  • 重命名字段(等于删+增)
  • 改主键 / 业务语义同名不同义
  • 复用 Protobuf 字段号
json
# 示意:新增可选字段带默认
{
  "type": "record",
  "name": "Click",
  "fields": [
    {"name": "event_id", "type": "string"},
    {"name": "user_id", "type": "string"},
    {"name": "price", "type": "double"},
    {"name": "scene", "type": ["null", "string"], "default": null}
  ]
}

4. 发布纪律(工程流程) ​

  1. 改 schema PR:附兼容级别与升级顺序
  2. CI 门禁:compatibility 检查对 Topic 当前 subject
  3. 先消费后生产 或 相反:严格按级别
  4. 灰度生产者:监控反序列化错误率
  5. 破坏性变更:新 subject / 新 Topic,双写过渡,旧 Topic 冻结
bash
# Confluent 风格示意(工具名随栈替换)
# curl -X POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \
#   --data '{"schema": "..."}' \
#   http://registry:8081/subjects/click-value/versions

# 兼容性检查
# curl http://registry:8081/compatibility/subjects/click-value/versions/latest \
#   -H "Content-Type: application/vnd.schemaregistry.v1+json" \
#   -d '{"schema":"..."}'

5. 消费者防御性解析 ​

  • 忽略未知字段
  • 缺失字段走默认
  • 类型不符合:进死信 Topic,勿卡死主消费(除非强一致要求)
  • 指标:deserialize_error_rate、死信量
java
// 示意:Protobuf 未知字段保留;业务读 getScene() 有默认
ClickEvent ev = ClickEvent.parseFrom(bytes);
String scene = ev.getScene(); // proto3 默认空串

6. 与数仓 / CDC 的衔接 ​

  • ODS 保留原始字节 + schema id,便于回放
  • 入湖表加列用可空类型,避免炸作业
  • CDC 源 schema 变:要跟 Debezium 等链路的兼容策略对齐(见 CDC 入湖)

7. 多消费者矩阵 ​

发布前填表:

消费者语言当前版本是否支持新字段升级窗口
Flink 作业 AJavav3是T+0
入湖 JobSparkv3需发版T+1
审计脚本Pythonv2否阻塞项

有一个「否」且级别不允许 → 禁止上生产。

8. 回滚策略 ​

  • Registry 不要随便删 subject 版本(权限管控)
  • 生产者回滚到旧版本二进制
  • 破坏性错误:切流量到旧 Topic 或停写

常见坑(至少牢记这些) ​

  1. JSON 无契约,靠「大家注意」
    类型悄变,下游半夜炸。

  2. 只改生产者不改消费者顺序
    与声明的兼容级别相反,必出反序列化错误。

  3. 重命名当无损
    对二进制约定是破坏性变更。

  4. 复用字段号 / 同名改义
    历史数据被「成功解析」成错误业务含义,比直接失败更可怕。

  5. 兼容检查只对 latest,不管历史
    老消费者 / 延迟作业读旧段失败;用 TRANSITIVE。

  6. 错误当脏数据丢掉且无死信
    资金/订单类消息静默丢,对账对不齐。

如何验收(可量化) ​

  • [ ] CI 兼容性检查通过记录留存
  • [ ] 升级顺序演练:按文档先消费或先生产,错误率 ≈ 0
  • [ ] 注入「新可选字段」消息,旧消费者仍可处理
  • [ ] 注入破坏性变更被门禁拦截(负向用例)
  • [ ] 死信 / 错误指标看板存在;Lag 不因反序列化失败螺旋
  • [ ] ODS 可按 schema id 回放一周内样本
bash
# 负向:故意不兼容应失败
echo "expect: compatibility = false"

# 正向:消费滞后作业追旧数据
kafka-consumer-groups.sh --bootstrap-server $BS --group g_replay --describe
sql
-- 入湖后新列可空率(新字段)
SELECT
  COUNT(*) AS cnt,
  SUM(CASE WHEN scene IS NULL THEN 1 ELSE 0 END) AS scene_null
FROM ods_click_di
WHERE dt = '${biz_date}';
text
验收画像:
- deserialize_error_rate < 0.01%
- 新字段在新版本生产者比例与灰度一致
- 无因 schema 导致的作业重启风暴

相关面试题 ​

占位:与题库小程序对齐后补链接(建议关联:Schema Registry、Avro 兼容、Kafka 演进、反序列化失败治理)。


改写说明:结构与判断标准参考 Wood 原创数仓/面试文,已改写成可执行清单;未包含未公开公司数据与内部表名。