3、特征生命周期中的血缘:从定义到下线的全流程追踪

特征不是写出来就完事的。它从诞生到退役,要经历定义、开发、验证、上线、监控、下线六个阶段。每个阶段都会产生新的血缘关系,也会继承上一阶段的血缘信息。

我见过不少团队,特征上线后出了问题,根本不知道它依赖哪些原始数据。更可怕的是,某个上游表被删了,结果三天后才发现特征已经产出空值。嗯,这就是血缘追踪没做到位。

3.1 特征定义阶段:血缘的起点

特征定义阶段,说白了就是「这个特征是什么、从哪里来」。这时候的血缘关系最原始,也最关键。

核心血缘信息

  • 特征名称:全局唯一标识,比如 user_7d_avg_order_amount
  • 特征类型:连续型、离散型、文本型、向量型
  • 数据来源:原始表名、字段名、数据源类型(MySQL / Kafka / Hive)
  • 业务含义:特征代表什么业务指标,方便后续理解
  • 定义人:谁定义的,出了问题找谁

我个人习惯在特征定义时就给它打上「血缘标签」。比如:

{
  "feature_name": "user_7d_avg_order_amount",
  "feature_type": "continuous",
  "source": {
    "table": "ods.order_info",
    "field": "amount",
    "type": "hive",
    "partition": "dt"
  },
  "transform": "avg(amount) over (partition by user_id order by dt rows between 6 preceding and current row)",
  "owner": "zhang_san",
  "created_at": "2024-01-15"
}

这个 JSON 就是特征的「出生证明」。后续所有血缘关系都会基于它展开。

我的经验:定义阶段最容易遗漏的是「数据源变更频率」。我曾经遇到一个特征,定义时写的是每天更新一次,结果上游表改成了实时写入,特征产出时间完全对不上。所以我现在会在血缘信息里加上 source_refresh_frequency 字段。

3.2 特征开发阶段:血缘的扩展

开发阶段,特征从「定义」变成「可执行的代码」。这时候血缘关系会大幅扩展。

为什么会这样?因为开发过程中会引入中间表、临时表、UDF、模型文件等依赖项。每个依赖项都是一条新的血缘边。

3.2.1 代码级血缘

特征开发代码里,藏着最细粒度的血缘关系。我建议用静态代码分析工具自动提取:

-- 特征:user_7d_avg_order_amount
-- 血缘自动提取结果
-- 输入表:ods.order_info (字段: user_id, amount, dt)
-- 中间表:tmp.user_7d_order_summary (字段: user_id, total_amount, order_count)
-- 输出表:dwd.user_feature_daily (字段: user_id, feature_name, feature_value)
-- 依赖函数:avg(), sum(), count()
-- 依赖模型:无

你看,一条 SQL 就能提取出完整的血缘链路。我习惯把这个信息写进特征元数据里,方便后续排查。

3.2.2 数据流血缘

除了代码,特征开发还涉及数据流的上下游关系。比如:

  • 输入数据流:从 Kafka Topic 消费实时数据
  • 处理逻辑:Flink 作业的算子链
  • 输出数据流:写入 Redis / HBase / 特征存储

这些数据流关系,在实时特征场景下尤其重要。我记得有一次,某个实时特征突然延迟了 10 分钟,查了半天才发现是上游 Kafka Topic 的 partition 数被改了,导致消费并发度不够。

避坑指南:开发阶段的血缘信息一定要自动化采集。我曾经见过一个团队,全靠人工维护 Excel 表格记录特征依赖,结果上线三个月后,Excel 和实际代码完全对不上。血的教训。

3.3 特征验证阶段:血缘的校验

验证阶段,我们要确认特征的血缘关系是否正确。说白了,就是「你声称依赖的那些数据,真的能用吗?」

我一般会做三件事:

  1. 血缘完整性检查:特征定义里的所有依赖项,在开发代码里是否都有对应实现?有没有遗漏的中间表?
  2. 血缘一致性检查:开发环境、测试环境、生产环境的血缘关系是否一致?表名、字段名、分区字段有没有差异?
  3. 血缘可达性检查:从原始数据到特征输出,整条链路是否通畅?有没有断点?

一个实用的检查脚本

def check_feature_lineage(feature_name):
    lineage = get_feature_lineage(feature_name)
    for node in lineage.nodes:
        if node.type == 'table':
            if not table_exists(node.name):
                raise Exception(f"表 {node.name} 不存在")
            for field in node.fields:
                if not field_exists(node.name, field):
                    raise Exception(f"字段 {field} 在表 {node.name} 中不存在")
    print(f"特征 {feature_name} 血缘检查通过")

3.4 特征上线阶段:血缘的固化

特征上线后,血缘关系就进入了「固化」状态。这时候的血缘信息,会成为后续运维和排查的依据。

我建议在上线时做一次血缘快照:

血缘维度 快照内容 更新频率
数据源血缘 原始表、字段、分区、数据源类型 每次上线
计算血缘 SQL 代码、UDF 版本、模型版本 每次上线
调度血缘 调度任务 ID、依赖任务、执行频率 每次上线
存储血缘 输出表、字段、存储引擎、TTL 每次上线

为什么要做快照?因为特征上线后,上游系统可能还在不断变化。有了快照,你就能知道「这个特征上线时依赖的是什么版本的数据」。我遇到过好几次,上游表改了 schema,但特征代码没更新,结果产出数据全错了。这时候翻出上线时的血缘快照,一眼就能看出问题。

3.5 特征监控阶段:血缘的动态追踪

特征上线后,血缘关系不是一成不变的。上游表可能被重命名、字段类型可能被修改、调度任务可能被调整。这些变化都会影响特征的血缘关系。

我个人习惯做两件事:

  • 血缘变更检测:定期扫描上游系统的元数据,和特征的血缘快照做对比。发现差异就告警。
  • 血缘影响分析:当上游表发生变化时,自动分析哪些特征会受影响,并通知对应的负责人。

一个真实案例:我们曾经有一个特征依赖的 Hive 表被删除了,但因为血缘监控及时,系统在 5 分钟内就发出了告警,并自动切换到了备份表。如果没有血缘监控,这个特征可能会产出空值数据整整一天。

3.6 特征下线阶段:血缘的清理

特征下线时,血缘关系的作用是「反向影响分析」。你需要知道:

  • 这个特征被哪些下游特征或模型依赖?
  • 下线后,会不会影响其他系统的正常运行?
  • 依赖的原始数据表是否可以清理?

我建议在下线前做一次完整的血缘影响分析:

def analyze_downstream_impact(feature_name):
    downstream = get_downstream_features(feature_name)
    if downstream:
        print(f"警告:特征 {feature_name} 被以下特征依赖:")
        for f in downstream:
            print(f"  - {f}")
        print("请确认这些特征已不再使用后再下线")
    else:
        print(f"特征 {feature_name} 无下游依赖,可以安全下线")

避坑指南:特征下线后,不要立即删除血缘信息。我建议保留至少 30 天的血缘历史数据,方便后续审计和回溯。我曾经见过一个团队,特征下线后立刻删除了所有血缘记录,结果三个月后业务方说「这个特征我们还在用」,但已经找不到任何证据了。

3.7 全流程血缘关系总览

最后,我用一张图来总结特征全生命周期中的血缘关系:

特征全生命周期血缘关系图 定义阶段 开发阶段 验证阶段 上线阶段 监控阶段 下线阶段 定义阶段血缘 • 特征名称、类型、数据来源 • 业务含义、定义人 • 原始表名、字段名、分区信息 开发/验证阶段血缘 • 代码级血缘:SQL、UDF、模型 • 数据流血缘:Kafka、Flink、Redis • 调度血缘:任务ID、依赖关系 上线/监控阶段血缘 • 血缘快照:上线时的完整依赖 • 变更检测:上游元数据变化 • 影响分析:变更影响范围 下线阶段血缘 • 下游依赖分析 • 反向影响评估 • 血缘历史保留(至少30天)

这张图展示了特征从定义到下线的完整流程。每个阶段都有对应的血缘信息需要记录和维护。你想想看,如果没有这些血缘关系,特征出了问题就像大海捞针。有了它,你就能顺着血缘链路一路排查下去,快速定位问题根因。