Skip to content

Commit bd54519

Browse files
committed
feat: first spark attempt
1 parent 1e8fc57 commit bd54519

9 files changed

Lines changed: 618 additions & 77 deletions

File tree

demos/demos-v2.yaml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -314,20 +314,23 @@ demos:
314314
description: Git branch to download demo files from
315315
default: main
316316
openlineage-trino-airflow:
317-
description: Showcase OpenLineage data lineage from Trino and Airflow, visualized in Marquez
317+
description: Showcase OpenLineage data lineage from Trino, Airflow and Spark over a shared Iceberg lakehouse, visualized in Marquez
318318
documentation: https://docs.stackable.tech/home/stable/demos/openlineage-trino-airflow
319319
stackableStack: openlineage
320320
labels:
321321
- trino
322322
- airflow
323+
- spark
323324
- openlineage
324325
- marquez
325326
- data-lineage
327+
- iceberg
328+
- minio
326329
manifests:
327330
- plainYaml: demos/openlineage-trino-airflow/rbac.yaml
328331
- plainYaml: demos/openlineage-trino-airflow/enable-and-run-dag.yaml
329332
supportedNamespaces: []
330333
resourceRequests:
331-
cpu: 5000m
332-
memory: 12Gi
333-
pvc: 15Gi
334+
cpu: 10500m
335+
memory: 22Gi
336+
pvc: 40Gi # 10Gi MinIO + 3x8Gi PostgreSQL; data is tiny (tpch.tiny)

demos/openlineage-trino-airflow/rbac.yaml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
---
2-
# Allow the enable-and-run Job (running under the namespace's default ServiceAccount) to wait for the
3-
# Airflow StatefulSets to become ready via `kubectl rollout status`.
2+
# Grants two things to every ServiceAccount in the namespace (see the ClusterRoleBinding below):
3+
# * the enable-and-run Job waits for the Airflow StatefulSets via `kubectl rollout status`
4+
# (pods + statefulsets get/list/watch), and
5+
# * the Airflow DAG's inlined SparkKubernetesOperator/Sensor creates and polls the SparkApplication
6+
# (sparkapplications create/get/list/watch).
47
apiVersion: rbac.authorization.k8s.io/v1
58
kind: ClusterRole
69
metadata:
@@ -22,6 +25,15 @@ rules:
2225
- get
2326
- list
2427
- watch
28+
- apiGroups:
29+
- spark.stackable.tech
30+
resources:
31+
- sparkapplications
32+
verbs:
33+
- get
34+
- list
35+
- watch
36+
- create
2537
---
2638
apiVersion: rbac.authorization.k8s.io/v1
2739
kind: ClusterRoleBinding

docs/modules/demos/pages/openlineage-trino-airflow.adoc

Lines changed: 156 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
= openlineage-trino-airflow
2-
:description: Demo showcasing OpenLineage data lineage emitted by Trino and Airflow and visualized in Marquez, enabled with minimal configuration.
2+
:description: Demo showcasing OpenLineage data lineage emitted by Trino, Airflow and Spark over a shared Iceberg lakehouse and visualized in Marquez, enabled with minimal configuration.
33

44
:openlineage: https://openlineage.io/
55
:marquez: https://marquezproject.ai/
66
:trino-openlineage-docs: https://trino.io/docs/current/admin/event-listeners-openlineage.html
77
:airflow-openlineage-docs: https://airflow.apache.org/docs/apache-airflow-providers-openlineage/stable/index.html
8+
:spark-openlineage-docs: https://openlineage.io/docs/integrations/spark/
89

9-
This demo shows how two Stackable Data Platform (SDP) products - Apache Trino and Apache Airflow - can emit
10-
{openlineage}[OpenLineage{external-link-icon}^] events with a single configuration toggle each, and how those events
11-
come together in {marquez}[Marquez{external-link-icon}^], the OpenLineage reference metadata server and UI.
10+
This demo shows how three Stackable Data Platform (SDP) products - Apache Trino, Apache Airflow and Apache Spark - can
11+
emit {openlineage}[OpenLineage{external-link-icon}^] events with a single configuration toggle each, and how those events
12+
come together in {marquez}[Marquez{external-link-icon}^], the OpenLineage reference metadata server and UI. Trino and
13+
Spark share an Apache Iceberg lakehouse on MinIO (S3), so a table written by Trino and read by Spark links the two
14+
engines' lineage into one graph.
1215

1316
Install this demo on an existing Kubernetes cluster:
1417

@@ -33,9 +36,9 @@ This demo should not be run alongside other demos.
3336

3437
To run this demo, your system needs at least:
3538

36-
* 5 https://kubernetes.io/docs/tasks/debug/debug-cluster/resource-metrics-pipeline/#cpu[cpu units{external-link-icon}^] (core/hyperthread)
37-
* 12GiB memory
38-
* 15GiB disk storage
39+
* 10 https://kubernetes.io/docs/tasks/debug/debug-cluster/resource-metrics-pipeline/#cpu[cpu units{external-link-icon}^] (core/hyperthread)
40+
* 24GiB memory
41+
* 128GiB disk storage
3942
4043
== Overview
4144

@@ -47,24 +50,44 @@ This demo will
4750
listener so that every query emits table- and column-level lineage.
4851
** *Airflow*: A workflow orchestrator. The native `apache-airflow-providers-openlineage` provider is enabled so that
4952
DAG and task runs - including the datasets they read and write - are reported as lineage.
50-
** *PostgreSQL*: Metadata database for Airflow.
53+
** *Spark*: A distributed data-processing engine. The `openlineage-spark` listener is enabled via `sparkConf` so that
54+
the Spark job's reads and writes are reported as lineage.
55+
** *Hive metastore* + *MinIO* (S3): back the shared Apache Iceberg `lakehouse` catalog that Trino and Spark both use.
56+
** *PostgreSQL*: Metadata databases for Airflow and the Hive metastore.
5157
** *Marquez*: The OpenLineage reference server + web UI (with its own PostgreSQL), used here as the consuming
5258
application that collects and visualizes the lineage. Marquez is only part of this demo, not an SDP product.
53-
* Mount and trigger a single Airflow DAG (`openlineage_demo`) that runs a small ELT pipeline in Trino
59+
* Mount and trigger a single Airflow DAG (`openlineage_demo`) that runs an ELT pipeline in Trino and then a Spark
60+
aggregation job
5461
* Let you explore the resulting lineage graph in the Marquez UI
5562

56-
The DAG builds a layered set of tables using Trino's built-in `tpch` catalog as the source and the in-memory `memory`
57-
catalog as the sink:
63+
The DAG reads from Trino's built-in `tpch` catalog and writes Iceberg tables into the shared `lakehouse` catalog. Trino
64+
produces `lakehouse.demo.customer_orders`; Spark then reads that same Iceberg table and writes
65+
`lakehouse.demo.orders_by_nation`:
5866

5967
[source]
6068
----
6169
tpch.tiny.customer ─┐
62-
├─> memory.demo.customer_orders
70+
├─(Trino)─> lakehouse.demo.customer_orders ─(Spark)─> lakehouse.demo.orders_by_nation
6371
tpch.tiny.orders ──┘
6472
----
6573

66-
Both products report to the *same* Marquez instance (`http://marquez:5000`): Trino emits the per-query dataset lineage,
67-
while Airflow emits the job/task run events (and dataset lineage via the Trino SQL operator).
74+
All three products report to the *same* Marquez instance (`http://marquez:5000`): Trino emits per-query dataset lineage
75+
(from its event listener), Airflow emits the DAG/task run events _plus_ the dataset lineage of every step, and Spark
76+
emits the read/write lineage of its job.
77+
78+
The single, connected end-to-end graph lives in the *Airflow* namespace (`airflow-openlineage-demo`). Airflow is the
79+
orchestrator and a single OpenLineage producer, so it names every dataset consistently: the Trino SQL tasks report
80+
`tpch.tiny.*` -> `lakehouse.demo.customers/orders` -> `lakehouse.demo.customer_orders`, and the Spark task is made to
81+
declare its own input/output (`customer_orders` -> `orders_by_nation`) under the *same* dataset identities. That shared
82+
`lakehouse.demo.customer_orders` node is what joins the Trino ELT and the Spark step into one graph.
83+
84+
[NOTE]
85+
====
86+
Marquez keys datasets by `namespace + name`, and each engine names the *same* physical Iceberg table differently
87+
(Trino's listener by its coordinator URI, Spark by the S3 location / Hive metastore). So Trino's own event-listener
88+
lineage and Spark's own job lineage show up as separate, self-contained views next to the unified Airflow graph - each
89+
producer's perspective on the same pipeline. The connected end-to-end flow is the Airflow one.
90+
====
6891

6992
== How OpenLineage is enabled
7093

@@ -95,6 +118,81 @@ AIRFLOW__OPENLINEAGE__NAMESPACE="airflow-openlineage-demo"
95118

96119
See the {airflow-openlineage-docs}[Airflow OpenLineage provider docs{external-link-icon}^] for details.
97120

121+
*Spark* loads the `openlineage-spark` listener and points it at Marquez purely through `sparkConf` on the
122+
`SparkApplication` (plus the package on the classpath via `deps.packages`):
123+
124+
[source,properties]
125+
----
126+
spark.extraListeners=io.openlineage.spark.agent.OpenLineageSparkListener
127+
spark.openlineage.transport.type=http
128+
spark.openlineage.transport.url=http://marquez:5000
129+
spark.openlineage.namespace=spark-openlineage-demo
130+
spark.app.name=orders-by-nation
131+
# parent* are injected at task runtime by the Airflow operator (see below) so they carry the
132+
# current run's identity - they link the Spark run to its Airflow task:
133+
spark.openlineage.parentJobNamespace=airflow-openlineage-demo
134+
spark.openlineage.parentJobName=openlineage_demo.spark_orders_by_nation
135+
spark.openlineage.parentRunId=<this Airflow task's OpenLineage run id>
136+
----
137+
138+
See the {spark-openlineage-docs}[OpenLineage Spark integration docs{external-link-icon}^] for details.
139+
140+
[NOTE]
141+
====
142+
The Stackable `SparkKubernetesOperator`/`Sensor` are not bundled in the Airflow image, so the demo DAG inlines a small
143+
in-cluster implementation. It does three things worth calling out, each a lever for cleaner lineage:
144+
145+
* It creates the `SparkApplication` *and waits for it in the same task* (no separate sensor task, so no extra empty
146+
"monitor" job in the graph).
147+
* It sets `spark.openlineage.parentJobNamespace/parentJobName/parentRunId` from the running task's OpenLineage identity
148+
(via the provider's `lineage_run_id`/`lineage_job_name` macros). This makes the Spark run report the Airflow task as
149+
its *root*, so the real Spark job is reachable from the DAG in Marquez.
150+
* It implements `get_openlineage_facets_on_complete` to declare the Spark step's input (`customer_orders`) and output
151+
(`orders_by_nation`) under the same dataset identities Airflow uses for the Trino tables. Airflow cannot introspect
152+
the out-of-process Spark job, so this explicit declaration is what draws the edge into the ELT graph.
153+
====
154+
155+
=== Getting clean, connected lineage
156+
157+
Enabling the listeners is only half the story - a few extra choices make the graph readable and connected. The demo
158+
applies them so it can double as a reference:
159+
160+
[cols="1,2"]
161+
|===
162+
| Lever | Effect
163+
164+
| `spark.openlineage.parent*` (from the Airflow task's run id)
165+
| Links the Spark run to its Airflow task (the Spark run's `root` becomes the task), so the orchestrated Spark job is
166+
reachable from the DAG rather than floating in its own namespace.
167+
168+
| `get_openlineage_facets_on_complete` on the Spark operator
169+
| Declares the Spark step's datasets under the Trino tables' identities, joining Spark to the ELT graph in the Airflow
170+
namespace.
171+
172+
| `CREATE OR REPLACE TABLE` (one statement per task, not `DROP` + `CREATE`)
173+
| One SQL statement per task means one lineage-bearing job, instead of an extra empty job for the throwaway `DROP`.
174+
175+
| One operator that submits *and* waits
176+
| Avoids a second, dataset-less sensor task appearing as an empty node.
177+
178+
| `spark.app.name` as a Spark *config* (not only `SparkSession.builder.appName`)
179+
| The listener reads the app name at `SparkContext` start; setting it in config gives the Spark jobs a stable name.
180+
|===
181+
182+
[NOTE]
183+
====
184+
Some rough edges remain and are *inherent to today's OpenLineage integrations*, not misconfiguration:
185+
186+
* Airflow's SQL provider emits an empty *task* job plus a `.query.N` sub-job that carries the datasets - the datasets
187+
live on the sub-job.
188+
* `openlineage-spark` (in k8s cluster mode) still names its internal application-root job `unknown` and leaves
189+
`spark_applicationDetails.applicationName` null, even with `spark.app.name` set.
190+
* The DAG-level `openlineage_demo` job and the `create_schema` (DDL) job have no datasets by nature.
191+
192+
These do not affect the correctness of the data flow; they are visible artifacts of how the current integrations model
193+
jobs.
194+
====
195+
98196
== List deployed Stackable services
99197

100198
To list the installed Stackable services run the following command:
@@ -112,11 +210,15 @@ $ stackablectl stacklet list --namespace openlineage_demo
112210
└─────────┴─────────┴───────────────────┴──────────────────────────────────────────┴─────────────────────────────────┘
113211
----
114212

213+
The `hive` metastore also appears as a stacklet.
214+
115215
include::partial$instance-hint.adoc[]
116216

117217
[NOTE]
118218
====
119-
Marquez is not an SDP product, so it does not appear in `stackablectl stacklet list`.
219+
Marquez, MinIO and the Spark job are not shown by `stackablectl stacklet list`: Marquez and MinIO are not SDP products,
220+
and the Spark job runs as a short-lived `SparkApplication` submitted by the Airflow DAG rather than a long-running
221+
stacklet.
120222
====
121223

122224
== Explore the lineage in Marquez
@@ -132,29 +234,55 @@ Then open http://localhost:5000 in your browser.
132234

133235
Use the namespace selector at the top to switch between the namespaces that produced the lineage:
134236

135-
* the Trino namespace (named after `openlineage-event-listener.trino.uri`, i.e. `http://trino-coordinator:8080`),
136-
which contains the `tpch.tiny.*` and `memory.demo.*` datasets and the queries connecting them, and
137-
* `airflow-openlineage-demo`, which contains the `openlineage_demo` DAG and its tasks.
237+
* `airflow-openlineage-demo` - the *connected end-to-end graph*. It contains the `openlineage_demo` DAG, its tasks, and
238+
the datasets `tpch.tiny.*`, `lakehouse.demo.customers/orders/customer_orders` and `lakehouse.demo.orders_by_nation`.
239+
* the Trino namespace (named after `openlineage-event-listener.trino.uri`, i.e. `http://trino-coordinator:8080`) - the
240+
Trino event listener's own view, with column-level lineage for each query.
241+
* `spark-openlineage-demo` (plus `s3://lakehouse` and `hive://...`) - the Spark job's own view of its read/write.
242+
243+
To see the full flow, open the `lakehouse.demo.orders_by_nation` dataset (or the `openlineage_demo.spark_orders_by_nation`
244+
task) in the `airflow-openlineage-demo` namespace and expand upstream. You get one connected graph:
138245

139-
Selecting the `memory.demo.customer_orders` dataset and expanding upstream shows the full graph back to
140-
`tpch.tiny.customer` and `tpch.tiny.orders`, including column-level lineage for the individual queries.
246+
[source]
247+
----
248+
tpch.tiny.customer ─▶ load_customers ─▶ customers ─┐
249+
├─▶ customer_orders ─▶ lakehouse.demo.customer_orders
250+
tpch.tiny.orders ─▶ load_orders ─▶ orders ──┘ │
251+
252+
spark_orders_by_nation ─▶ lakehouse.demo.orders_by_nation
253+
----
254+
255+
The real Spark job is not lost: because of the parent-run link, the Spark run reports the
256+
`openlineage_demo.spark_orders_by_nation` task as its *root*, so you can drill from that task into the Spark run itself
257+
(in the `spark-openlineage-demo` namespace).
258+
259+
[TIP]
260+
====
261+
Open a *dataset* or a *task* node - for example `spark_orders_by_nation` or the `orders_by_nation` dataset - not the
262+
top-level `openlineage_demo` DAG node. Airflow reports the DAG-level job with no inputs/outputs of its own, so that one
263+
node looks empty; the data flow hangs off the task and dataset nodes.
264+
====
141265

142266
[NOTE]
143267
====
144-
Trino and Airflow name datasets independently, so a given table may appear under both the Trino namespace (from the
145-
Trino event listener) and the Airflow namespace (from the SQL operator's extractor). This is expected - each producer
146-
reports lineage from its own perspective.
268+
The Trino event-listener view (`trino://...`) and the Spark job's own view (`spark-openlineage-demo` / `s3://lakehouse`
269+
/ `hive://...`) are separate, self-contained graphs alongside the Airflow one. Marquez identifies datasets by
270+
`namespace + name`, and each engine names the same physical Iceberg table differently (Trino by its coordinator URI,
271+
Spark by the storage location / metastore), so they do not auto-merge. That is expected: each producer reports lineage
272+
from its own perspective, and the *connected* end-to-end view is the Airflow one, where a single producer (the
273+
orchestrator) names every dataset consistently.
147274
====
148275

149276
== Summary
150277

151-
This demo showed that both Trino and Airflow can emit OpenLineage data to Marquez with only a small amount of
152-
configuration and no changes to the SDP operators or CRDs. It serves as a validation of the approach ahead of adding
153-
first-class OpenLineage support to the platform.
278+
This demo showed that Trino, Airflow and Spark can all emit OpenLineage data to Marquez with only a small amount of
279+
configuration and no changes to the SDP operators or CRDs, and that the Airflow orchestrator can tie a Trino ELT and a
280+
Spark job - sharing an Iceberg lakehouse - into a single connected lineage graph. It serves as a validation of the
281+
approach ahead of adding first-class OpenLineage support to the platform.
154282

155283
== Where to go from here
156284

157285
* Trigger the `openlineage_demo` DAG again from the Airflow UI and watch new runs appear in Marquez.
158-
* Run your own queries against Trino (for example via `stackablectl stacklet list` to find the coordinator endpoint)
159-
and see them show up as additional lineage.
286+
* Query the Spark output from Trino: `SELECT * FROM lakehouse.demo.orders_by_nation ORDER BY total_spent DESC` (use
287+
`stackablectl stacklet list` to find the coordinator endpoint) and see the query show up as additional lineage.
160288
* Read more about {openlineage}[OpenLineage{external-link-icon}^] and {marquez}[Marquez{external-link-icon}^].

0 commit comments

Comments
 (0)