外观
消息 Schema 演进与兼容
难度与前置
难度:进阶
前置:生产或消费过带结构的消息;知道加字段可能弄挂下游。
读完能做什么:制定兼容策略(向前/向后/全兼容),用 Schema Registry 或约定约束演进,并用兼容性检查与回放验收。
适用场景
- Topic 已有多消费者(实时作业、入湖、审计),需加字段/改类型
- 引入 Avro/Protobuf + Schema Registry
- 数仓 ODS 贴源半结构化,频繁「多一个字段」
不适用:
- 单生产者单消费者且可同步发版停机切换(仍建议留版本号)
- 纯日志字符串无结构约定、且明确不解析字段
- 与 分区设计 无关的容量问题(分开治)
真实场景(一条)
点击 Topic 原 JSON 无 schema 约束。某次客户端把 price 从数字改成字符串,Flink 作业反序列化失败,重启循环,Lag 飙升。改造:上 Avro + Registry,兼容级别 BACKWARD;CI 对 schema 做兼容性门禁;消费者对未知字段忽略、对缺失字段用默认值。此后加可选字段可灰度,破坏性变更走新 Topic / 新版本策略。
怎么做(步骤清单)
1. 先选序列化与「真相源」
| 格式 | 优点 | 注意 |
|---|---|---|
| JSON | 易读 | 无强约束,易 silently 变坏 |
| Avro | Schema + 默认值友好 | 需 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. 发布纪律(工程流程)
- 改 schema PR:附兼容级别与升级顺序
- CI 门禁:
compatibility检查对 Topic 当前 subject - 先消费后生产 或 相反:严格按级别
- 灰度生产者:监控反序列化错误率
- 破坏性变更:新 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 作业 A | Java | v3 | 是 | T+0 |
| 入湖 Job | Spark | v3 | 需发版 | T+1 |
| 审计脚本 | Python | v2 | 否 | 阻塞项 |
有一个「否」且级别不允许 → 禁止上生产。
8. 回滚策略
- Registry 不要随便删 subject 版本(权限管控)
- 生产者回滚到旧版本二进制
- 破坏性错误:切流量到旧 Topic 或停写
常见坑(至少牢记这些)
JSON 无契约,靠「大家注意」
类型悄变,下游半夜炸。只改生产者不改消费者顺序
与声明的兼容级别相反,必出反序列化错误。重命名当无损
对二进制约定是破坏性变更。复用字段号 / 同名改义
历史数据被「成功解析」成错误业务含义,比直接失败更可怕。兼容检查只对 latest,不管历史
老消费者 / 延迟作业读旧段失败;用 TRANSITIVE。错误当脏数据丢掉且无死信
资金/订单类消息静默丢,对账对不齐。
如何验收(可量化)
- [ ] CI 兼容性检查通过记录留存
- [ ] 升级顺序演练:按文档先消费或先生产,错误率 ≈ 0
- [ ] 注入「新可选字段」消息,旧消费者仍可处理
- [ ] 注入破坏性变更被门禁拦截(负向用例)
- [ ] 死信 / 错误指标看板存在;Lag 不因反序列化失败螺旋
- [ ] ODS 可按 schema id 回放一周内样本
bash
# 负向:故意不兼容应失败
echo "expect: compatibility = false"
# 正向:消费滞后作业追旧数据
kafka-consumer-groups.sh --bootstrap-server $BS --group g_replay --describesql
-- 入湖后新列可空率(新字段)
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 原创数仓/面试文,已改写成可执行清单;未包含未公开公司数据与内部表名。