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)。

事件类型主要有三种:

  1. START:Run 开始。此时会声明 Job 定义、输入 Dataset、输出 Dataset。
  2. RUNNING:Run 正在执行。可以更新 Facet。
  3. 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 核心关系全景图

下面这张图,是我自己画的核心概念关系图。你看一眼,就能明白它们是怎么协作的。

OpenLineage 核心概念关系图 Job(菜谱) Run(下厨) Dataset(输入食材) Dataset(输出成品) Facet(备注/标签) Job 定义做什么,Run 记录怎么做,Dataset 标明数据流向,Facet 补充细节。 事件(Event)驱动整个生命周期,每次状态变化都会触发一次事件上报。

4.7 事件驱动的工作流

最后,咱们把整个流程串起来。一个典型的数据血缘采集过程是这样的:

  1. 任务启动:调度系统(如 Airflow)触发一个 Job。OpenLineage 客户端发送 START 事件,包含 Job 定义和输入/输出 Dataset。
  2. 任务执行中:如果任务有中间状态变化,可以发送 RUNNING 事件,更新 Facet。
  3. 任务结束:任务成功或失败,发送 COMPLETE 或 FAIL 事件。此时血缘关系最终确定。
  4. 后端处理:Marquez 或其他 OpenLineage 后端接收事件,构建血缘图。
  5. 可视化查询:用户通过 UI 或 API 查询血缘关系。

说白了,整个过程就是 事件驱动、状态机流转。你只要保证每个关键节点都正确发送了事件,血缘图就能自动构建出来。

总结一下:Run 是执行实例,Job 是任务定义,Dataset 是数据实体,Facet 是扩展元数据。它们通过事件模型串联,构成了 OpenLineage 的血缘采集骨架。理解了这个骨架,后面讲自动化采集和可视化,你就知道数据是怎么流进去、怎么流出来的了。