4. OpenLineage 核心概念:Run、Job、Dataset、Facet、事件模型解析
好,咱们进入 OpenLineage 的核心地带。说实话,我第一次看 OpenLineage 的规范文档时,也被那一堆概念搞得有点晕。Run、Job、Dataset、Facet……它们到底是什么关系?
别急,我用一个最朴素的比喻帮你理清:把一次数据加工想象成做一顿饭。
- Job 就是你的菜谱 —— 它定义了“做什么”和“怎么做”。
- Run 是你今天实际下厨的这一次 —— 它记录了“什么时候做的”、“谁做的”、“结果如何”。
- Dataset 是食材和成品 —— 输入是菜,输出是菜。
- Facet 是菜谱上的备注、厨师的笔记、食材的标签 —— 用来补充细节。
嗯,这个框架一旦建立,后面就好理解了。咱们一个一个拆开看。
4.1 Run:一次具体的执行
Run 是 OpenLineage 里最活跃的概念。它代表 Job 的一次具体运行实例。
每个 Run 都有一个全局唯一的 runId(UUID)。它记录了这次运行的开始时间、结束时间、状态(运行中、成功、失败、取消等)。
我在项目中遇到过一个问题:同一个 Spark 任务,每天跑 10 次,如果不用 Run 区分,血缘图就乱成一锅粥。Run 就是用来区分“哪一次”的。
核心要点:Run 是动态的,有生命周期的。它从 START 事件开始,到 COMPLETE 或 FAIL 事件结束。
4.2 Job:一个可重复执行的逻辑单元
Job 是静态的定义。它描述了一个 可重复执行的数据处理任务。
比如你写了一个 Spark SQL 脚本,每天凌晨跑一次。这个脚本本身就是一个 Job。它有一个 name(比如 etl.daily_user_agg)和一个 namespace(比如 spark://prod-cluster)。
Job 和 Run 的关系是 一对多。一个 Job 可以有很多次 Run。你想想看,如果没有 Job 这个概念,你每次 Run 都得重新描述一遍“我要做什么”,那多冗余。
| 概念 | 性质 | 举例 |
|---|---|---|
| Job | 静态定义 | spark://cluster/job=user_etl |
| Run | 动态实例 | runId=550e8400-e29b-41d4-a716-446655440000 |
4.3 Dataset:输入与输出
Dataset 就是数据本身。它可以是表、文件、主题(Kafka Topic)等。
在 OpenLineage 里,Dataset 分为 输入(Input) 和 输出(Output)。一个 Job 读取输入 Dataset,经过处理,写出输出 Dataset。这就是血缘关系的核心边。
每个 Dataset 也有 namespace 和 name。比如:
namespace: postgresql://prod-db
name: public.orders
namespace: hdfs://data-lake
name: /warehouse/dwd/dwd_order_di
我个人习惯把 Dataset 的命名空间设计得跟实际存储路径一致。这样查问题的时候,一眼就能定位到数据在哪。
小技巧:Dataset 的 Facet 可以记录 schema 信息。当表结构变更时,血缘图会自动感知。我曾经靠这个发现了一个上游表字段被删导致的下游任务失败。
4.4 Facet:扩展元数据的万能口袋
Facet 是 OpenLineage 最灵活的设计。说白了,它就是 附加在 Run、Job、Dataset 上的元数据片段。
为什么需要 Facet?因为不同的数据系统有不同的特性。Spark 有 Spark 的配置,Airflow 有 Airflow 的上下文,DBT 有 DBT 的模型信息。OpenLineage 不可能预知所有场景,所以它设计了一个扩展点 —— Facet。
常见的 Facet 类型:
- Job Facet:比如
spark_job_details,记录 Spark 的配置参数。 - Run Facet:比如
airflow_run_details,记录 DAG ID 和 Task ID。 - Dataset Facet:比如
schema、column_lineage、data_quality_metrics。
嗯,这里要注意:Facet 是 可选的。你不必为每个 Run 都填满所有 Facet。按需使用就好。
避坑指南:我曾经在一个项目中,把所有能想到的信息都塞进 Facet,结果事件体积暴涨,Kafka 直接报警。Facet 虽好,不要贪多。只放那些对血缘分析真正有用的元数据。
4.5 事件模型:OpenLineage 的通信语言
好,概念都清楚了。那它们怎么串起来?答案就是 事件(Event)。
OpenLineage 的事件模型基于 Run 的生命周期。每次状态变化,都会触发一个事件。事件通过 HTTP 或 Kafka 发送到 OpenLineage 后端(比如 Marquez)。
事件类型主要有三种:
- START:Run 开始。此时会声明 Job 定义、输入 Dataset、输出 Dataset。
- RUNNING:Run 正在执行。可以更新 Facet。
- COMPLETE / FAIL / ABORT:Run 结束。最终确定血缘关系。
一个典型的事件 JSON 结构长这样:
{
"eventType": "START",
"eventTime": "2024-01-15T10:00:00Z",
"run": {
"runId": "550e8400-e29b-41d4-a716-446655440000"
},
"job": {
"namespace": "spark://prod-cluster",
"name": "etl.user_agg"
},
"inputs": [
{
"namespace": "hdfs://data-lake",
"name": "/raw/users"
}
],
"outputs": [
{
"namespace": "hdfs://data-lake",
"name": "/dwd/user_agg"
}
],
"runFacets": {},
"jobFacets": {},
"datasetFacets": {}
}
你想想看,只要把这些事件按时间顺序串起来,就能还原出完整的数据血缘图。从哪个表来,经过什么处理,去了哪个表,一目了然。
4.6 核心关系全景图
下面这张图,是我自己画的核心概念关系图。你看一眼,就能明白它们是怎么协作的。
4.7 事件驱动的工作流
最后,咱们把整个流程串起来。一个典型的数据血缘采集过程是这样的:
- 任务启动:调度系统(如 Airflow)触发一个 Job。OpenLineage 客户端发送
START事件,包含 Job 定义和输入/输出 Dataset。 - 任务执行中:如果任务有中间状态变化,可以发送
RUNNING事件,更新 Facet。 - 任务结束:任务成功或失败,发送
COMPLETE或FAIL事件。此时血缘关系最终确定。 - 后端处理:Marquez 或其他 OpenLineage 后端接收事件,构建血缘图。
- 可视化查询:用户通过 UI 或 API 查询血缘关系。
说白了,整个过程就是 事件驱动、状态机流转。你只要保证每个关键节点都正确发送了事件,血缘图就能自动构建出来。
总结一下:Run 是执行实例,Job 是任务定义,Dataset 是数据实体,Facet 是扩展元数据。它们通过事件模型串联,构成了 OpenLineage 的血缘采集骨架。理解了这个骨架,后面讲自动化采集和可视化,你就知道数据是怎么流进去、怎么流出来的了。