3、血缘元数据模型:特征与上游数据源的映射关系、特征与下游模型的依赖关系、元数据存储方案
好,咱们进入第三章。这一章要聊的是血缘元数据模型——说白了,就是怎么把特征的血缘关系给“建模”出来,存起来,用起来。
我刚开始做特征平台的时候,觉得血缘不就是“特征从哪来、到哪去”嘛,画个图就完了。后来发现,真正落地的时候,光画图远远不够。你得有一套清晰的元数据模型,才能支撑起自动化的血缘追踪、影响分析和数据治理。
这一章,我会从三个核心角度展开:特征与上游数据源的映射关系、特征与下游模型的依赖关系,以及元数据存储方案。嗯,咱们一个一个来。
3.1 特征与上游数据源的映射关系
先看上游。一个特征是怎么来的?通常是从原始数据经过一系列加工得到的。这个加工过程,可能是简单的字段映射,也可能是复杂的多表Join、聚合计算,甚至是一整套机器学习特征工程。
我个人习惯把这种映射关系分成三个层次:
- 字段级映射:特征直接对应某个数据表的某个字段。比如“用户年龄”特征,直接来自用户表的age字段。
- 表达式级映射:特征是通过一个或多个字段计算得到的。比如“用户消费金额”特征,是订单表的price * quantity。
- 逻辑级映射:特征涉及多表关联、条件过滤、窗口函数等复杂逻辑。比如“用户最近30天购买次数”,需要关联订单表、订单明细表,还要做时间窗口过滤。
我在项目中遇到过一种情况:同一个特征,在不同版本里,上游数据源居然不一样。比如“用户活跃度”特征,V1版本用的是用户登录日志,V2版本改成了用户行为埋点。如果不记录这种映射关系的变化,后面排查问题会非常痛苦。
核心原则:每个特征必须记录其完整的上游数据源链路,包括数据表、字段、转换逻辑、版本信息。缺一不可。
下面是一个简化的映射关系示例,我用JSON格式来表示:
{
"feature_id": "user_30d_purchase_count",
"version": "v2.1",
"upstream": [
{
"table": "ods.order_info",
"fields": ["user_id", "order_amount", "create_time"],
"type": "source"
},
{
"table": "ods.order_detail",
"fields": ["order_id", "product_id"],
"type": "source"
}
],
"transform": {
"logic": "JOIN order_info ON order_id, FILTER create_time >= now() - 30d, GROUP BY user_id, COUNT(order_id)",
"engine": "Spark SQL",
"script_path": "hdfs://feature-etl/user_30d_purchase_count_v2.1.sql"
}
}
你想想看,有了这样的模型,当上游某个表发生Schema变更时,我们就能快速定位到哪些特征会受影响。这就是血缘追踪的价值所在。
3.2 特征与下游模型的依赖关系
说完上游,再看下游。特征最终是要被模型消费的。一个模型可能依赖几十甚至上百个特征,一个特征也可能被多个模型共用。
这种依赖关系,我建议用有向无环图(DAG)来建模。每个特征是一个节点,每个模型也是一个节点,边表示“模型依赖特征”。
为什么要强调DAG?因为在实际生产中,特征和模型之间可能存在循环依赖——比如特征A依赖模型B的输出,模型B又依赖特征A。这种循环在血缘追踪里是灾难,必须避免。
我曾经在一个推荐系统项目里,发现一个特征居然间接依赖了它自己。排查了两天才找到原因:特征工程脚本里不小心引用了模型输出的中间结果,而那个模型又用了这个特征。嗯,从那以后,我强制要求所有特征和模型的血缘关系必须是无环的。
下面是一个典型的依赖关系示例:
| 模型名称 | 依赖特征列表 | 特征数量 | 上线时间 |
|---|---|---|---|
| 用户流失预警模型 v3 | user_30d_purchase_count, user_last_login_days, user_avg_order_amount | 12 | 2024-03-15 |
| 商品推荐模型 v2 | user_30d_purchase_count, product_click_rate, product_category | 8 | 2024-04-01 |
| 风控评分模型 v1 | user_30d_purchase_count, user_register_days, user_device_count | 15 | 2024-04-10 |
你看,user_30d_purchase_count这个特征被三个模型同时依赖。如果这个特征出了问题,三个模型都会受影响。这就是为什么我们要做影响分析——当特征需要下线或修改时,能提前知道会波及哪些模型。
小技巧:在元数据模型中,给每个依赖关系加上“依赖类型”字段。比如“强依赖”(模型必须依赖该特征)和“弱依赖”(模型可以降级使用)。这样在做影响分析时,可以区分优先级。
3.3 元数据存储方案
模型有了,关系清楚了,接下来就是怎么存。元数据存储方案,我把它分成三个维度来考虑:
- 存储引擎选择:关系型数据库还是图数据库?
- 数据模型设计:表结构怎么设计?
- 版本管理:血缘关系变了怎么办?
3.3.1 存储引擎选择
我个人经验是:图数据库 + 关系型数据库混合使用。
图数据库(比如Neo4j)天然适合存储血缘关系。节点和边的模型,查询起来非常直观。比如“找出所有依赖特征A的模型”,在图数据库里就是一条简单的路径查询。
关系型数据库(比如MySQL)则适合存储元数据的详细信息,比如特征的描述、版本、负责人、上线时间等。这些信息用关系表存起来,查询和更新都很方便。
我建议的做法是:
- 图数据库负责血缘关系的存储和查询,提供API给血缘追踪和影响分析使用。
- 关系型数据库负责元数据的详细信息,提供API给元数据管理平台使用。
- 两者通过特征ID和模型ID进行关联。
3.3.2 数据模型设计
关系型数据库这边,我常用的表结构是这样的:
| 表名 | 主要字段 | 说明 |
|---|---|---|
| feature_metadata | feature_id, name, version, description, owner, status, create_time | 特征元数据主表 |
| model_metadata | model_id, name, version, description, owner, status, create_time | 模型元数据主表 |
| feature_upstream | feature_id, upstream_table, upstream_field, transform_logic, version | 特征与上游数据源的映射关系 |
| model_feature_dependency | model_id, feature_id, dependency_type, create_time | 模型与特征的依赖关系 |
| lineage_change_log | change_id, entity_type, entity_id, old_value, new_value, change_time, operator | 血缘变更日志 |
图数据库这边,节点类型包括Feature、Model、DataSource,边类型包括DERIVED_FROM(特征来源于数据源)、DEPENDED_BY(模型依赖特征)。
3.3.3 版本管理
血缘关系是会变的。特征可能升级,模型可能迭代,数据源可能重构。如果不做版本管理,历史血缘关系就丢了,排查历史问题时会非常被动。
我建议的做法是:
- 每个特征和模型都维护一个版本号,血缘关系跟着版本走。
- 每次血缘关系变更,都记录到
lineage_change_log表中。 - 提供“时间旅行”查询能力:给定一个时间点,能查到那个时刻的血缘关系快照。
避坑指南:我曾经在一个项目里,没有做版本管理,结果特征升级后,旧模型还在用旧特征,新模型已经用了新特征。排查问题时,根本分不清哪个版本的血缘关系是对的。后来花了整整一周才把历史血缘关系补全。所以,版本管理一定要从第一天就开始做。
3.4 核心知识体系
为了让你更直观地理解这一章的知识结构,我画了一张图:
这张图把这一章的核心内容串起来了。从上游数据源到特征,再到下游模型,中间是映射关系和存储方案。最下面是核心能力——血缘追踪、影响分析、数据治理,这三者是一体的。