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 核心知识体系

为了让你更直观地理解这一章的知识结构,我画了一张图:

特征血缘元数据模型核心结构 上游数据源 ODS表 / 日志 / 消息队列 特征 特征ID / 版本 / 转换逻辑 下游模型 模型ID / 版本 / 依赖类型 映射关系:字段级 → 表达式级 → 逻辑级 存储方案:图数据库(血缘关系)+ 关系型数据库(元数据详情) 版本管理:特征/模型版本号 + 血缘变更日志 + 时间旅行查询 核心能力:血缘追踪 → 影响分析 → 数据治理 上游变更影响评估 / 特征下线影响分析 / 历史血缘回溯

这张图把这一章的核心内容串起来了。从上游数据源到特征,再到下游模型,中间是映射关系和存储方案。最下面是核心能力——血缘追踪、影响分析、数据治理,这三者是一体的。