2. 数据血缘的元数据采集:元数据来源、采集策略与频率
元数据采集,说白了就是给数据血缘「找原料」。没有原料,后面分析得再漂亮也是白搭。我刚开始做血缘项目时,就吃过这个亏——工具搭好了,结果发现连最基本的表依赖关系都没采全,整个血缘图就是一张破网。
这一节,咱们把元数据采集这件事拆开揉碎了讲。你想想看,元数据从哪来?怎么采?采多少?多久采一次?这些问题搞不清楚,血缘分析就是空中楼阁。
2.1 元数据的三大来源
元数据不会自己长腿跑进你的系统里。它藏在三个地方:ETL 作业、SQL 语句、系统日志。我习惯把这三种来源比作「三路侦察兵」,各有所长,也各有盲区。
| 来源类型 | 核心原理 | 优点 | 缺点 |
|---|---|---|---|
| ETL 作业 | 解析调度工具(如 Airflow、DataStage)的 DAG 定义 | 结构清晰,上下游关系明确 | 依赖工具规范,自定义节点难解析 |
| SQL 解析 | 对 SQL 语句做词法/语法分析,提取表、字段引用 | 粒度细,能到字段级 | 复杂 SQL(CTE、子查询)容易解析失败 |
| 系统日志 | 从数据库审计日志、查询日志中抓取执行记录 | 真实反映运行时依赖 | 数据量大,噪音多,需要大量过滤 |
我的建议:不要只用一种来源。ETL 作业给骨架,SQL 解析给肌肉,系统日志给血液。三路合流,才能拼出一张可信的血缘图。
2.1.1 ETL 作业解析
ETL 工具是血缘分析最直接的入口。每个 ETL 作业本质上就是一个「数据管道」的定义——从哪读、怎么算、往哪写。我在项目中遇到过用 Airflow 的场景,DAG 文件里每个 Task 的上下游关系,直接映射成血缘的边。
举个例子,一个典型的 ETL 任务:
# 伪代码:从 ODS 层读取订单表,清洗后写入 DWD 层
ods_orders = HiveOperator(
task_id='read_ods_orders',
sql='SELECT * FROM ods.orders WHERE dt = "{{ ds }}"'
)
dwd_orders = HiveOperator(
task_id='write_dwd_orders',
sql='''
INSERT OVERWRITE TABLE dwd.orders PARTITION(dt = "{{ ds }}")
SELECT order_id, user_id, amount, status
FROM ods.orders
WHERE status IS NOT NULL
'''
)
ods_orders >> dwd_orders # 定义上下游
解析这个 DAG,我们能直接得到:ods.orders → dwd.orders 这条血缘边。但注意,这里只能看到表级关系,字段级的映射还得靠 SQL 解析。
小技巧:解析 ETL 作业时,别只盯着 DAG 文件。很多调度系统(比如 Azkaban、Oozie)的配置文件里藏着参数化信息,比如 ${table_name} 这种变量,需要做变量替换后才能拿到真实表名。
2.1.2 SQL 解析
SQL 解析是血缘分析里最核心、也最头疼的部分。为什么?因为 SQL 太灵活了。子查询、CTE、窗口函数、UDF……每一种语法糖都可能让解析器翻车。
我常用的 SQL 解析工具是 sqlparse(Python)和 ANTLR(Java)。前者轻量,适合快速原型;后者语法规则全,适合生产环境。
看一个简单的字段级血缘解析示例:
-- 原始 SQL
CREATE TABLE dwd.user_order_summary AS
SELECT
u.user_id,
u.user_name,
COUNT(o.order_id) AS order_cnt,
SUM(o.amount) AS total_amount
FROM ods.users u
JOIN ods.orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.user_name;
-- 解析后得到的字段级血缘
ods.users.user_id → dwd.user_order_summary.user_id
ods.users.user_name → dwd.user_order_summary.user_name
ods.orders.order_id → dwd.user_order_summary.order_cnt (经过 COUNT)
ods.orders.amount → dwd.user_order_summary.total_amount (经过 SUM)
这里要注意,order_cnt 和 total_amount 是经过聚合函数转换的,血缘关系里需要标记「转换类型」。我习惯在血缘边上加一个 transform 属性,记录是直接映射、聚合、还是表达式计算。
避坑指南:我曾经踩过一个坑——解析 CTE(公用表表达式)时,递归 CTE 会让解析器陷入死循环。后来我加了递归深度限制(默认 10 层),超过就截断并打上「不完整」标记。记住,血缘分析追求的是「可信」,不是「完美」。
2.1.3 系统日志采集
系统日志是最后一道防线。当 ETL 作业和 SQL 解析都拿不到完整信息时(比如临时查询、adhoc 任务),日志能帮你补上缺口。
常见的日志来源:
- 数据库审计日志:记录谁、在什么时间、执行了什么 SQL
- 查询引擎日志:Hive、Spark、Presto 的执行计划日志
- 应用日志:业务系统打印的 SQL 执行记录
日志采集的难点在于「噪音过滤」。一个生产数据库每天可能产生几百万条查询日志,但其中 90% 都是 SELECT 查询,而且很多是重复的。我一般会做三层过滤:
- 去重:相同 SQL 模板只保留一条
- 过滤 DDL/DML:只保留 CREATE TABLE AS、INSERT、MERGE 等写操作
- 提取表名:用正则或解析器从 SQL 文本中提取表名
2.2 采集策略与频率
元数据采集不是「一次性买卖」。数据环境在变,表结构在变,ETL 作业也在变。你得想清楚:多久采一次?每次采多少?
我一般把采集策略分成三种:
| 策略 | 适用场景 | 频率建议 | 风险 |
|---|---|---|---|
| 定时全量 | 数据仓库分层稳定,变更不频繁 | 每天凌晨 1 次 | 采集窗口长,可能影响业务 |
| 事件触发 | ETL 作业频繁变更,需要实时感知 | 每次 DAG 部署后触发 | 依赖事件机制,可能漏采 |
| 增量轮询 | 日志类数据,数据量大但变化小 | 每 15 分钟一次 | 需要维护偏移量,有延迟 |
我的经验:别把采集频率设得太高。我曾经在一个项目里把 SQL 解析频率设成每 5 分钟一次,结果解析集群 CPU 直接打满,业务查询都受影响。后来改成「ETL 作业变更时触发全量 + 每天凌晨增量补采」,问题就解决了。
2.3 增量采集与全量采集
这两个概念不复杂,但很多人用错了地方。我简单说下我的理解。
全量采集:把所有的元数据从头到尾扫一遍。适合第一次搭建血缘系统,或者元数据源本身数据量不大(比如几千张表)。
增量采集:只采集上次采集之后发生变化的部分。适合生产环境,数据量大,但变更范围小。
怎么判断用哪种?我一般看三个指标:
- 数据量:表数量超过 1 万张,建议用增量
- 变更频率:每天变更的表超过 5%,全量更省事
- 采集窗口:全量采集能在 30 分钟内跑完,可以接受全量
实际项目中,我常用的是「全量 + 增量」混合模式:
# 伪代码:混合采集策略
def collect_metadata():
# 1. 先检查上次全量时间
last_full_time = get_last_full_collect_time()
# 2. 如果超过 24 小时没全量,触发全量
if now() - last_full_time > timedelta(hours=24):
full_collect_all_tables()
return
# 3. 否则只采集变更的表
changed_tables = get_changed_tables(since=last_full_time)
for table in changed_tables:
incremental_collect(table)
一个实用技巧:增量采集的关键是「变更检测」。我习惯在元数据源上加一个 last_modified 字段,或者用数据库的 CDC(Change Data Capture)机制。如果没有这些,可以对比表结构的哈希值——把表名、字段名、字段类型拼成字符串算 MD5,变了就采。
2.4 知识体系总览
说了这么多,咱们用一张图把整个元数据采集的知识体系串起来。这张图我画了很多遍,每次给团队新人讲血缘分析时都会拿出来。
这张图从左到右看,就是元数据采集的完整链路:来源 → 策略 → 模式 → 存储。每个环节都有多种选择,没有银弹。我个人的做法是:先跑通全量,再优化增量,最后根据业务场景调整策略和频率。