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 查询,而且很多是重复的。我一般会做三层过滤:

  1. 去重:相同 SQL 模板只保留一条
  2. 过滤 DDL/DML:只保留 CREATE TABLE AS、INSERT、MERGE 等写操作
  3. 提取表名:用正则或解析器从 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 知识体系总览

说了这么多,咱们用一张图把整个元数据采集的知识体系串起来。这张图我画了很多遍,每次给团队新人讲血缘分析时都会拿出来。

元数据采集知识体系 ETL 作业解析 SQL 解析 系统日志采集 定时全量 事件触发 增量轮询 全量采集 增量采集 混合模式 血缘元数据存储(图数据库 / 关系库) 核心原则:来源互补、策略匹配、模式灵活,最终产出可信的血缘元数据

这张图从左到右看,就是元数据采集的完整链路:来源 → 策略 → 模式 → 存储。每个环节都有多种选择,没有银弹。我个人的做法是:先跑通全量,再优化增量,最后根据业务场景调整策略和频率。