数据血缘基础概念

聊流批一体排障,绕不开一个核心概念——数据血缘。说白了,它就是数据的「族谱」。你想想看,一条数据从源头流到最终报表,中间经过了多少算子、多少转换、多少存储?出了问题,你总得知道它从哪来、到哪去、中间被谁改过吧?

我刚开始做数据平台时,遇到过一个线上事故:某天凌晨的离线报表突然多了一倍销售额。排查了整整两天,最后发现是实时管道里一个字段映射配错了,影响了离线数仓的中间表。那时候我就想,要是能有个「数据地图」一键定位问题源头,该多好。嗯,这就是数据血缘要解决的事。

什么是数据血缘

数据血缘,记录的是数据从产生、流转、加工到最终消费的完整链路。它不只是「谁依赖谁」这么简单,还包含字段级别的映射关系、转换逻辑、时间戳、版本号等元信息。

核心三要素:

  • 上游(Source):数据从哪来,比如 Kafka Topic、Hive 表、API 接口
  • 下游(Target):数据往哪去,比如 ClickHouse 报表、Redis 缓存、下游 Topic
  • 转换(Transformation):中间经历了什么,比如 Flink 算子、Spark SQL、存储过程

我习惯把血缘分为两种粒度:

  • 表级血缘:表与表之间的依赖关系。比如「表 A → 表 B → 表 C」,适合做整体链路梳理。
  • 字段级血缘:字段与字段之间的映射关系。比如「A.col1 + A.col2 → B.col3」,适合做精确排障。

举个例子,你在 Flink SQL 里写了个 INSERT INTO sink_table SELECT id, name, age + 1 FROM source_table。字段级血缘会记录:sink_table.id ← source_table.id、sink_table.name ← source_table.name、sink_table.age ← source_table.age + 1。排障时,你一眼就能看出 age 字段被加过 1,不用再去翻代码。

我的经验:字段级血缘才是排障利器。表级血缘只能告诉你「谁影响了谁」,字段级血缘才能告诉你「具体哪个字段出了问题」。我在做实时数仓时,80% 的排障场景都靠字段级血缘定位。

血缘的元数据模型

要落地血缘,先得把模型定清楚。我常用的元数据模型包含以下几个核心实体:

实体 说明 关键属性
Dataset 数据集,可以是表、Topic、文件 名称、类型、存储位置、Schema
Job 作业,负责数据转换 作业名、类型(Flink/Spark)、版本
Field 字段,数据集的最小单元 字段名、类型、描述、默认值
Lineage 血缘关系,连接上下游 上游ID、下游ID、转换表达式、时间戳
Operation 操作记录,谁在什么时候做了什么 操作人、操作时间、操作类型(创建/修改/删除)

这些实体之间的关系,我习惯用一张图来展示:

数据血缘元数据模型 Dataset 表 / Topic / 文件 Field 字段级元信息 Job Flink / Spark 作业 Lineage 上下游依赖关系 Operation 操作审计日志 包含 执行 记录 审计 模型设计要点 • Dataset 和 Field 是一对多关系,一个数据集包含多个字段 • Lineage 连接上下游 Dataset,同时记录转换表达式 • Operation 记录谁在什么时候修改了血缘关系,用于审计

这个模型看起来简单,但落地时有个坑——版本管理。数据管道是会演进的,今天这张表的 Schema 和昨天可能不一样。如果不记录版本,血缘关系就是「死」的,排障时查到的可能是过时的链路。

我曾经踩过的坑:有一次实时作业升级了 Flink 版本,字段映射逻辑变了,但血缘系统没更新。排障时查到的血缘还是旧版本,导致定位方向完全错了。后来我强制要求:每次作业发布,血缘元数据必须同步更新版本号。

血缘关系的存储方式

模型定好了,接下来就是存的问题。目前主流方案有两种:图数据库和关系型数据库。我两种都用过,各有优劣。

图数据库(Neo4j、JanusGraph)

图数据库天然适合存血缘。为什么?因为血缘本身就是一张图——Dataset 是节点,Lineage 是边。查询「某张表的上游有哪些」这种场景,图数据库一次遍历就能搞定。

我举个实际例子:

// Neo4j Cypher 查询:找某张表的所有上游
MATCH (t:Dataset {name: 'dwd_order'})<-[:DEPENDS_ON*1..5]-(upstream)
RETURN upstream.name, upstream.type

这个查询会递归向上找 5 层上游,返回所有依赖的表或 Topic。在关系型数据库里,你得写递归 CTE,性能还差一截。

维度 图数据库 关系型数据库
查询性能 多跳查询快,毫秒级 递归查询慢,秒级甚至分钟级
模型灵活性 Schema-less,新增关系类型方便 需要提前设计表结构,改起来麻烦
事务支持 弱,ACID 支持有限 强,适合高频写入
运维成本 高,需要专门团队 低,DBA 就能搞定
适合场景 血缘查询、影响分析 元数据存储、审计日志

关系型数据库(MySQL、PostgreSQL)

关系型数据库的优势在于成熟、稳定、运维简单。如果你的血缘数据量不大(百万级以内),用 MySQL 完全够用。我早期的一个项目就是用 MySQL 存的,表结构大概这样:

CREATE TABLE lineage (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  upstream_dataset_id BIGINT NOT NULL,
  downstream_dataset_id BIGINT NOT NULL,
  transformation_expr TEXT,
  job_id BIGINT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  version INT DEFAULT 1,
  INDEX idx_upstream (upstream_dataset_id),
  INDEX idx_downstream (downstream_dataset_id)
);

查询上游时,用递归 CTE:

WITH RECURSIVE upstream_chain AS (
  SELECT upstream_dataset_id, downstream_dataset_id, 1 AS depth
  FROM lineage
  WHERE downstream_dataset_id = ?
  UNION ALL
  SELECT l.upstream_dataset_id, l.downstream_dataset_id, uc.depth + 1
  FROM lineage l
  JOIN upstream_chain uc ON l.downstream_dataset_id = uc.upstream_dataset_id
  WHERE uc.depth < 10
)
SELECT * FROM upstream_chain;

这个写法在数据量小的时候还行,一旦超过千万级,递归查询的延迟就上来了。我遇到过最夸张的一次,查 5 层上游花了 30 秒,业务方直接投诉。

我的建议:如果团队有图数据库运维能力,优先用图数据库存血缘关系。如果团队小、运维资源有限,先用关系型数据库顶着,等数据量上来再迁移。我现在的做法是「双写」——关系型数据库存元数据,图数据库存血缘关系,各取所长。

混合方案

其实还有一种折中方案:关系型数据库 + 缓存层。血缘关系存 MySQL,但把查询结果预热到 Redis 或 Elasticsearch。查询时走缓存,写入时异步更新。这样既保证了写入的可靠性,又提升了查询性能。

我上一个项目就是这么干的:

  • 写入:Flink 作业解析血缘关系,写入 MySQL
  • 缓存:定时任务把血缘关系构建成图结构,写入 Redis(用 Set 存上下游关系)
  • 查询:前端查血缘时,优先走 Redis,Redis 没有再查 MySQL

这套方案支撑了日均 10 万+ 的血缘查询,P99 延迟控制在 50ms 以内。嗯,够用了。

总结一下:

  • 数据血缘是排障的「导航地图」,字段级血缘比表级血缘更实用
  • 元数据模型要包含 Dataset、Field、Job、Lineage、Operation 五个核心实体,别忘了版本管理
  • 存储方式选型:图数据库适合查询密集型场景,关系型数据库适合写入密集型场景,混合方案是折中之道