2、数据血缘基础理论:血缘的粒度、采集方式与存储模型
数据血缘,说白了就是数据从哪来、经过谁、变成啥、去了哪。在特征平台里,血缘分析是排查特征异常、评估变更影响的核心工具。我刚开始做特征平台时,总觉得血缘就是个“数据地图”,后来踩过几次坑才明白——粒度不对、采集不全、存储拉胯,血缘就是一张废纸。
这一节,咱们把血缘的三个基础问题聊透:粒度、采集、存储。
2.1 血缘的粒度:你究竟要追踪到多细?
血缘的粒度,决定了你能回答什么级别的问题。我个人习惯把粒度分成三层:字段级、表级、任务级。
| 粒度 | 追踪对象 | 典型问题 | 我遇到的坑 |
|---|---|---|---|
| 字段级 | 列 → 列 | “用户年龄”这个字段是从哪张表的哪个列来的? | 字段级血缘缺失,排查特征异常时只能靠猜 |
| 表级 | 表 → 表 | 这张特征宽表依赖哪些源表? | 表级血缘够用,但定位具体列变更时不够精确 |
| 任务级 | ETL/Spark/Flink 任务 → 产出表 | 这个特征是由哪个任务产出的?任务挂了影响哪些下游? | 任务级血缘常被忽略,但它是运维止损的关键 |
我的建议:特征平台至少要做到“字段级 + 任务级”的双层血缘。表级血缘可以作为兜底,但别只依赖它。我在一个项目中只做了表级血缘,结果某个特征字段被上游改了类型,排查了整整两天——如果有字段级血缘,10 分钟就能定位。
2.2 血缘的采集方式:三种主流方案
采集血缘,没有银弹。不同场景用不同方式。我总结下来,主流方案就三种:解析、打点、逆向工程。
2.2.1 解析(Parse)
解析 SQL 或代码,提取输入输出关系。这是最常用的方式。
- 适用场景:Spark SQL、Hive、Flink SQL 等结构化查询
- 优点:准确、无侵入、可离线批量跑
- 缺点:对自定义 UDF、动态 SQL 支持差
举个例子,解析一条 SQL:
INSERT OVERWRITE TABLE dwd.user_feature PARTITION(dt='2024-01-01')
SELECT
u.user_id,
u.age,
o.total_amount
FROM ods.user u
JOIN ods.order o ON u.user_id = o.user_id
WHERE o.dt = '2024-01-01'
解析后,我们能得到:
- 表级:ods.user + ods.order → dwd.user_feature
- 字段级:u.user_id → user_id, u.age → age, o.total_amount → total_amount
避坑指南:我曾经用 ANTLR 写过一个 SQL 解析器,结果被各种方言(Hive、Spark、Presto)的语法差异搞到崩溃。后来改用 Calcite 的 SQL 解析器,兼容性好很多。如果你也要做解析,建议直接用 Calcite 或 Spark 自带的 Catalyst 分析器。
2.2.2 打点(Instrumentation)
在代码中埋点,运行时采集血缘信息。适合复杂逻辑、自定义算子。
- 适用场景:PySpark、Flink DataStream、自定义 ETL
- 优点:能覆盖解析搞不定的场景
- 缺点:有侵入性、影响性能、需要改造现有代码
举个例子,在 PySpark 中打点:
def read_with_lineage(spark, path, table_name):
df = spark.read.parquet(path)
# 记录血缘:输入表
lineage.record_input(table_name, path, df.schema)
return df
def write_with_lineage(df, path, table_name):
# 记录血缘:输出表 + 字段映射
lineage.record_output(table_name, path, df.schema)
df.write.parquet(path)
注意:打点方式容易漏采。我记得有一次,某个同事在代码里用了 df.withColumn 创建新字段,但打点代码没覆盖这个算子,导致字段级血缘断链。所以打点一定要覆盖所有数据变换算子,别偷懒。
2.2.3 逆向工程(Reverse Engineering)
从数据本身或元数据推断血缘。比如对比两张表的字段名、数据类型、数据分布。
- 适用场景:没有源码、没有打点、历史遗留系统
- 优点:无侵入、能补全缺失的血缘
- 缺点:不准确、只能推断、不能保证 100% 正确
举个例子,如果两张表都有 user_id 字段,且数据类型都是 STRING,我们可以推断它们之间有血缘关系。但如果有字段重命名,逆向工程就抓瞎了。
我的经验:逆向工程只能作为“补丁”用。我建议优先用解析 + 打点,覆盖 90% 的场景,剩下的 10% 用逆向工程兜底。别指望逆向工程能解决所有问题——它连字段重命名都搞不定。
2.3 血缘的存储模型:怎么存才能查得快?
血缘数据存不好,查询时就是灾难。我见过有人把血缘存成 JSON 大字段,结果查一个字段的上游要扫全表。下面是我推荐的三种存储模型。
2.3.1 图存储(Neo4j / JanusGraph)
血缘天然是图结构:节点是表、字段、任务,边是依赖关系。图数据库查询血缘路径非常快。
- 优点:支持多跳查询、路径分析、影响范围分析
- 缺点:运维成本高、学习曲线陡
举个例子,查询“字段 A 的所有上游字段”:
MATCH (f:Field {name: 'user_id'})<-[:PRODUCES*]-(upstream:Field)
RETURN upstream
2.3.2 关系型存储(MySQL / PostgreSQL)
用关系表存储血缘关系。简单、稳定、团队都熟。
- 优点:运维简单、容易集成
- 缺点:多跳查询性能差、递归查询写起来麻烦
表结构示例:
CREATE TABLE field_lineage (
id BIGINT PRIMARY KEY,
upstream_table VARCHAR(255),
upstream_field VARCHAR(255),
downstream_table VARCHAR(255),
downstream_field VARCHAR(255),
task_id VARCHAR(255),
created_at TIMESTAMP
);
CREATE INDEX idx_downstream ON field_lineage(downstream_table, downstream_field);
CREATE INDEX idx_upstream ON field_lineage(upstream_table, upstream_field);
2.3.3 文档存储(Elasticsearch)
把血缘关系存成文档,利用 ES 的倒排索引做快速搜索。
- 优点:搜索快、支持全文检索、适合做血缘搜索
- 缺点:不支持复杂图遍历、更新成本高
文档示例:
{
"downstream_table": "dwd.user_feature",
"downstream_field": "age",
"upstreams": [
{"table": "ods.user", "field": "age", "task": "etl_user_feature"},
{"table": "ods.profile", "field": "age", "task": "etl_user_feature"}
],
"task_id": "etl_user_feature_20240101"
}
我的选择:特征平台的血缘存储,我推荐“图存储 + 关系型”双写。图存储做实时查询和路径分析,关系型做离线批量和审计。ES 可以作为搜索层,但不是必须的。你想想看,如果只存关系型,查一个字段的完整血缘链路,可能要递归几十次——图数据库一次查询就搞定。
2.4 本章小结:一张图看懂血缘基础
下面这张图,把血缘的粒度、采集方式、存储模型串起来了。我每次给团队做血缘设计时,都会先画这张图——它能让所有人对齐认知。
嗯,这一节的内容就到这里。血缘的粒度、采集、存储,是后续所有血缘分析功能的基础。你在实际项目中,可以先从字段级血缘 + 解析采集 + 图存储开始,跑通后再逐步完善。别一开始就想搞大而全——我见过太多团队,血缘系统设计得无比复杂,最后没人用。