数据血缘基础概念
聊流批一体排障,绕不开一个核心概念——数据血缘。说白了,它就是数据的「族谱」。你想想看,一条数据从源头流到最终报表,中间经过了多少算子、多少转换、多少存储?出了问题,你总得知道它从哪来、到哪去、中间被谁改过吧?
我刚开始做数据平台时,遇到过一个线上事故:某天凌晨的离线报表突然多了一倍销售额。排查了整整两天,最后发现是实时管道里一个字段映射配错了,影响了离线数仓的中间表。那时候我就想,要是能有个「数据地图」一键定位问题源头,该多好。嗯,这就是数据血缘要解决的事。
什么是数据血缘
数据血缘,记录的是数据从产生、流转、加工到最终消费的完整链路。它不只是「谁依赖谁」这么简单,还包含字段级别的映射关系、转换逻辑、时间戳、版本号等元信息。
核心三要素:
- 上游(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 | 操作记录,谁在什么时候做了什么 | 操作人、操作时间、操作类型(创建/修改/删除) |
这些实体之间的关系,我习惯用一张图来展示:
这个模型看起来简单,但落地时有个坑——版本管理。数据管道是会演进的,今天这张表的 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 五个核心实体,别忘了版本管理
- 存储方式选型:图数据库适合查询密集型场景,关系型数据库适合写入密集型场景,混合方案是折中之道