3. 数据网格中的领域数据产品
好,咱们进入第三章。这一章我打算聊聊数据网格里最核心的概念——领域数据产品。说白了,数据网格就是把数据当成产品来经营,每个业务领域都要为自己的数据负责。那问题来了:这个产品长什么样?怎么用 Flink 把它做出来?版本怎么管?Schema 怎么演进?
我在好几个数据平台项目里都踩过类似的坑——数据团队辛辛苦苦把数据加工好,业务方却说“这不是我要的”。为什么?因为数据没有按照产品的思路去设计。今天咱们就把这块掰开揉碎了讲清楚。
3.1 数据产品的定义与设计
先说说什么是数据产品。我个人习惯把它定义为:面向特定消费场景、具备明确 SLA、可独立交付的数据资产。它不是一个表,也不是一个文件,而是一个有边界、有接口、有质量承诺的“数据服务”。
数据产品的四个核心属性:
- 领域归属:每个数据产品必须属于一个业务领域,比如订单域、用户域、库存域
- 输出契约:Schema、更新频率、延迟要求、数据质量指标
- 可发现性:有元数据描述,能被其他团队搜索到
- 自治性:领域团队可以独立发布、迭代,不依赖中央数据团队
举个例子。我在一个电商项目里做过“用户实时画像”这个数据产品。它的设计是这样的:
| 属性 | 内容 |
|---|---|
| 产品名称 | user_realtime_profile |
| 所属领域 | 用户域 |
| 输出格式 | Avro,包含 user_id、tag_map、last_update_time |
| 更新频率 | 秒级,事件驱动 |
| SLA | P99 延迟 < 500ms,数据可用性 99.9% |
| 消费者 | 推荐系统、广告系统、客服平台 |
你看,这个设计里没有“数据表”的概念,而是直接面向消费场景。推荐系统要的是“用户当前最感兴趣的标签”,广告系统要的是“用户最近30天的购买偏好”——这些都在 tag_map 里。
3.2 Flink 如何实现领域数据产品
那 Flink 在这个架构里扮演什么角色?说白了,Flink 就是数据产品的“生产线”。每个领域团队可以部署自己的 Flink 作业,把原始数据加工成领域数据产品。
我建议的做法是:一个数据产品对应一个 Flink 作业。这样职责清晰,出了问题也好排查。
来看一个实际的例子。假设我们要实现上面说的“用户实时画像”产品:
// 用户实时画像 Flink 作业核心逻辑
DataStream<UserEvent> events = env
.fromSource(kafkaSource, WatermarkStrategy.noWatermarks(), "user-events");
DataStream<UserProfile> profiles = events
.keyBy(event -> event.getUserId())
.process(new ProfileAggregationFunction())
.name("user-profile-aggregation");
// 输出到数据产品存储(Kafka Topic)
profiles.sinkTo(kafkaSink)
.name("user-profile-sink");
这里有个关键点:数据产品的输出必须是 Schema 化的。我强烈推荐用 Avro 或者 Protobuf,别用 JSON 裸奔。为什么?因为 Schema 就是数据产品的接口契约,没有 Schema 就没法做版本管理。
我的经验:在定义数据产品的输出 Schema 时,一定要预留扩展字段。比如用 Avro 的 union 类型或者 map 类型,这样后续加字段不会破坏兼容性。我曾经因为没预留扩展字段,导致一次 Schema 变更影响了 12 个下游系统,那叫一个惨。
再深入一点。Flink 作业怎么保证数据产品的 SLA?我一般会做三件事:
- 设置 Checkpoint 间隔:根据延迟要求调整,比如要求秒级延迟就把 checkpoint 间隔设到 10 秒
- 监控端到端延迟:在数据产品输出时打上时间戳,用 Flink Metrics 上报
- 配置告警规则:延迟超过阈值自动告警,必要时自动重启作业
3.3 数据产品的版本管理与 Schema 演进
好,这部分是重头戏。数据产品一旦上线,就会有多个消费者依赖它。你改 Schema 的时候,不能把所有人都搞崩了。
数据产品的版本管理,我总结了三层:
| 层级 | 管理内容 | Flink 中的实现 |
|---|---|---|
| Schema 版本 | Avro/Protobuf Schema 的兼容性演进 | Schema Registry + Avro Serializer |
| 数据产品版本 | 整个数据产品的 API 版本,比如 v1、v2 | Kafka Topic 命名 + 消费者组隔离 |
| 作业版本 | Flink 作业的代码版本,对应数据产品的内部实现 | Savepoint + 版本化部署 |
先说 Schema 演进。Flink 配合 Schema Registry 做 Avro 兼容性检查,这个套路我用了好几年了。核心规则是:
- 向后兼容:新 Schema 可以读旧数据(允许加字段,但要有默认值)
- 向前兼容:旧 Schema 可以读新数据(允许删字段,但不能删消费者正在用的字段)
- 完全兼容:同时满足向后和向前
举个例子,假设 v1 的 Schema 是这样的:
{
"type": "record",
"name": "UserProfile",
"fields": [
{"name": "user_id", "type": "string"},
{"name": "tags", "type": {"type": "map", "values": "string"}}
]
}
现在要加一个 last_active_time 字段,怎么做才兼容?
{
"type": "record",
"name": "UserProfile",
"fields": [
{"name": "user_id", "type": "string"},
{"name": "tags", "type": {"type": "map", "values": "string"}},
{"name": "last_active_time", "type": ["null", "long"], "default": null}
]
}
注意这里用了 union 类型 ["null", "long"],并且给了 default null。这样旧的消费者读到新数据时,last_active_time 字段会被解析为 null,不会报错。
避坑指南:我曾经在 Schema 演进时犯过一个低级错误——给新字段加了 default 值,但忘了检查下游消费者是否依赖这个字段的语义。结果下游有个系统把 null 当成了“用户从未活跃”,直接触发了错误的风控策略。所以,加字段不仅要考虑格式兼容,还要考虑语义兼容。
再说数据产品版本。当 Schema 变更无法做到兼容时,就需要升级数据产品的版本。我习惯的做法是:
- v1 版本输出到 topic user_profile_v1
- v2 版本输出到 topic user_profile_v2
- 两个版本并行运行一段时间,等所有消费者迁移完再下线 v1
Flink 作业怎么支持多版本?很简单,部署两个作业实例,分别消费不同的 source、输出到不同的 sink。等 v2 稳定了,用 Savepoint 停掉 v1 作业。
最后,我用一张图来总结数据产品的生命周期管理:
嗯,这张图基本把数据产品从设计到迭代的流程串起来了。你想想看,如果没有这套机制,每次改 Schema 都得通知所有下游,协调发布时间窗口,那得多痛苦。
总结一下这一章的核心:数据产品是数据网格的“一等公民”,Flink 是它的生产线,Schema Registry 是它的版本控制器。把这三点想清楚,你的数据网格架构就有了坚实的底座。