Ein Data Lakehouse mit Trino, Iceberg, Postgres und MinIO
Inhalt
Einführung
Nach den klassischen Data Warehouses, die man locker mit einer Modellierungsstrategie, einer Datenbank, einem Datenintegrationswerkzeug und einem BI Frontend umsetzen konnte, gab es zahlreiche konzeptionelle Weiterentwicklungen: BigData hat geholfen, den Kopf Richtung Streaming Data und unstrukturierten (Dokumenten-)daten zu drehen. Trennung von Storage und Compute hat uns auch schon dort beschäftigt und aus BigData mit Hadoop Backends wurde schließlich der Data Lake.
Mit den Lambda und Kappa Architekturen hatte man auch schon in den BigData Systemen eine Idee, wie man mit schnellen, unstrukturierten Informationen zur sofortigen Nutzung und parallel davon abgeleiteten modellierten Layern umgeht.
Nun sind Data Lakehouses ein weiteres Thema, welches die strukturierte Datenmodellierung der ursprünglichen Data Warehouses wieder zurückbringt, ohne die Errungenschaften zu vernachlässigen.
Oder ? Es hilft nichts, ich muss das ausprobieren ! Auch wenn ich schon in kommerziellen Projekten mit DataBricks, Snowflake, Python, PySpark Erfahrungen gemacht habe und einen Teil der Produktversprechen der Softwareanbieter kenne, möchte ich tiefer in die Systeme und Methoden einsteigen, am liebsten mit einer nicht proprietären Plattform, an der ich beliebig herumschrauben kann.
Ich benötige also erstmal einen Data Lake(-house) Softwarestack, den ich schnell und einfach auf meiner lokalen Hardware hochziehen kann, um dann spezielle Fragestellungen zu bearbeiten.
Die Recherche zeigt, dass ich nicht alleine mit der Idee bin. Es finden sich zahlreiche Artikel, die in die gleiche Richtung gehen :
Bei allen fehlt mir mal die eine mal die andere Komponente, also ran, ich baue mir einen eigenen Stack !
Ich baue einen Open Source Data Lakehouse Stack !
Erstmal simpel anfangen : eigentlich sollte doch nicht mehr als Storage, Compute und ein Katalog nötig sein ! Für lokales Ausprobieren erscheint zudem Docker / Docker Compose eine gute Wahl. Solche Projekte setze ich am liebsten auf meinen Linux Rechnern um, deshalb schreibe ich die zugehörigen Skripte mit Bash.
Die in diesem Text verwendeten Scripte habe ich im Iceduck Repository zusammengefasst, welches ich unter Codeberg Iceduck Repository zur freien Verfügung stelle. Der Name Iceduck kommt aus der ursprünglichen Idee, Iceberg Tables lediglich mit DuckDB zu verwalten. Das war aber doch etwas zu kurz gesprungen…
Das Repository enthält einen kompletten Stack, der mit git clone https://codeberg.org/pfabricius/iceduck.git auf Deinem Rechner landet und ausprobiert werden kann.
Ungeduldige können den Stack mit ./iceduck init sofort hochfahren und die Details aus dem Text anschliessend nachvollziehen. ./iceduck clean räumt den Stack wieder komplett ab, während mit den Parametern stop und start der Stack hoch- und runtergefahren wird, ohne persistierte Daten zu verlieren.
Alle Anderen finden im weiteren Text Details, wie der Stack schrittweise aufgebaut werden kann.
Storage mit MinIO
Da ich lokal auf meinem Rechner arbeiten möchte, fällt AWS S3 oder Google Cloud Storage als Backend aus, es bleibt z.B. MinIO. MinIO stellt glücklicherweise Docker images zur Verfügung.
Eine Docker-Compose Datei, welches ein Volume für Dateien verwendet, ist schnell gestrickt. Dazu baue ich ein Wrapper Skript, welches die MinIO Kommandozeile mc in einem weiteren temporären Container startet. Das Wrapper Skript verwendet die gleichen Umgebungsvariablen wie das docker-compose.yaml und benutzt ein eigenes Volume, damit die Konfigurationen und History der Kommandozeile zwischen Aufrufen erhalten bleiben.
Für den allerersten Start habe ich also vier Dateien :
- docker-compose.yaml definiert den MinIO Service in einem Docker Compose Setup. Ich verwende eine etwas ältere Version von MinIO, die noch die vollständige Adminstrationsfunktionalität in der WebUI mitbringt.
- etc/iceduck.env enthält die Variablen, die im Docker Compose verwendet werden. Für MinIO sind das die AWS_* und MINIO_* Variablen.
- bin/mc ist das Wrapperscript um die MinIO Kommandozeile, das sowohl interaktiv arbeitet aber auch Scriptdateien ausführen kann.
- files/init/prep_minio.sh enthält die mc Kommandos, um ein Bucket anzulegen
Die Initialisierung von MinIO wird durch iceduck init vorgenommen und enthält das Anlegen der Verzeichnisse für die Volumes, den Start des Containers sowie das Anlegen eines Buckets mit Hilfe des Wrapperscripts bin/mc. Wenn alles gut gegangen ist, kann ich anschliessend die MinIO WebUI starten und mich dort mit admin/password anmelden.
Super, in der Web-UI kann ich auch sehen, dass ein Bucket angelegt wurde, ich habe nun einen lokalen Object Store !!
Katalog mit Apache Polaris
Polaris installieren
In Apache Iceberg: The Definitive Guide lässt sich ausführlich über die Notwendigkeit eines Katalogs nachlesen. Dieser führt die Dateien in einen Bucket logisch zu Tabellen zusammenführt und stellt diese Informationen z.B. einer Query Engine zur Verfügung. Der Katalog ist damit ein permanenter eigenständiger Service, der seine Informationen z.B. als REST API zur Verfügung stellt. Da Apache Iceberg keine Software, sondern lediglich eine Spezifikation darstellt, gibt es auch mehrere Implementierung von Iceberg Katalogen.
Für meinen Stack fällt die Wahl auf Apache Polaris. Das Projekt ist zwar noch im Apache Incubator, enthält aber die vollständige Implementierung der Iceberg REST API und die Unterstützung zahlreicher weiterer Data Lake(House) Query Engines wie Trino, DremIO, Snowflake etc. ist zugesagt.
Als Backend für die Katalogdaten möchte ich eine relationale Datenbank verwenden. Alternativ wäre auch ein dateibasiertes Backend in S3/MinIO möglich, aber das erscheint mir “frickeliger”. Mit der Datenbank bin ich in der Lage mir die Metadaten des Katalogs auch mal auf relationaler Seite anzuschauen. Das sehe ich genauso als Mehrwert, wie die Tatsache, dass ein relationales Backend näher an einer produktiven Umgebung ist, als andere Varianten. Da PostgreSQL in der Regel gut unterstützt wird, verwende ich ein entsprechendes Image in meinem Stack. Das Postgres Image baue ich mit einem Dockerfile selber, da es für spätere Evaluierungen gleich noch die DuckDB Extension enthalten soll. Beim ersten Start des Stacks benötigt der Build deshalb ein wenig mehr Zeit.
Mit der Erweiterung des oben begonnenen docker-compose.yaml um die Services postgres und polaris geht es weiter. Wie schon bei MinIO verwenden die Servicedefinitionen die Environmentdatei etc/iceduck.env die nun um einige Einträge erweitert werden muss. Es kommen die POLARIS_, QUARKUS_ und POSTGRES_* Variablen hinzu.
In der Postgres Datenbankinstanz soll gleich bei der Initialisierung eine Datenbank mit Benutzer und Rechten für Polaris angelegt werden. Dafür kann in den Datenbankcontainer ein Verzeichnis mit Initialisierungsskripten gemountet werden. 01_iceduck_db.sh enthält den dazu notwendigen parameterisierten SQL Code. Da ich wie angekündigt auch noch mit der Postgres DuckDB Extension experimentieren möchte, lege ich dafür gleich eine seperate Datenbank namens iceduck an.
Damit wird bei einem iceduck init nun ein Postgres-Image Build durchgeführt, die DB gestartet und zwei Postgres Datenbanken samt Benutzer angelegt. Zudem wird ein Apache Polaris Service gestartet, der die Datenbank als Backend verwendet.
Das reicht aber noch nicht : in Polaris muss ein Katalog manuell durch einen Bootstrap Vorgang angelegt werden. Zudem müssen Benutzer, Rechte und Rollen definiert werden.
Polaris bootstrappen
Auch der Bootstrap Vorgang ist in iceduck init als Aufruf eines weiteren Wrapperskripts enthalten. Dieses ruft das bootstrap Kommando der Polaris Admin Tools auf. Unglücklicherweise sind diese Tools nicht Bestandteil des zuvor als Container gestarteten Apache Polaris Images sondern lediglich in einem weiteren polaris-admin-tool Image enthalten.
Der Bootstrap Vorgang legt einen Polaris Bereich/Namensraum/REALM sowie einen zugehörigen Root-Benutzer mit eigenem Secret an. Auch diese drei Parameter sind in etc/iceduck.env enthalten :
- POLARIS_REALM
- POLARIS_ROOT_CLIENT_ID
- POLARIS_ROOT_CLIENT_SECRET
Polaris initialisieren
Nun muss in diesem Polaris Realm noch ein Katalog angelegt werden. Dazu werden mit dem o.g. Root User mittels REST Calls ein Auth Tokens erstellt, der Katalog sowie Rollen und Rechte in Polaris gesetzt. Zur Vereinfachung gibt es ein weiteres, von iceduck init verwendetes Wrapperskript bin/iceshell, welches die Verwendung von curl Aufrufen mit den in etc/iceduck.env definierten Variablen vereinfacht.
Die Rest Calls, die für das Anlegen von Katalog, Rollen und Rechten verwendet werden. sind in files/init/prep_polaris.sh enthalten und werden von iceduck init verwendet.
Nun
Query Engines
Was jetzt noch fehlt ist ein Werkzeug, mit dem ich Iceberg Tabellen anlegen, befüllen und abfragen kann. Da gibt es reichlich Auswahl, wie z.B. hier dargestellt wird. Da ich mich in meinem Stack nur lokale und möglichst freie Produkte verwenden will, beschränke ich mich auf
- Trino, eine SQL Engine, die neben Iceberg auch andere Backends ansprechen und verbinden kann
- Spark stellt mit SparkSQL, PySpark, PyIceberg und weiteren Implementierungen ein ganzes Feld an Möglichkeiten bereit
DuckDB in Form der Postgres DuckDB Extension installiere ich auch in den Stack, aber eher als lesende Komponente.
Trino
Trino ist laut Website eine “distributed Query engine”, die verschiedene Datenbankbackends mit Hilfe von SQL ansprechen kann. Für den Iceduck Stack richte ich einen One-Node Trino Service ein, der gleichzeitig Koordinator und Worker ist. Dazu wird der Trino Service in das bekannte docker-compose.yaml eingebunden. Wie gehabt wird auch die etc/iceduck.env wieder zur Parameterisierung verwendet. Sie beinhaltet für Trino lediglich den Port, mit dem die Web-UI auf dem Docker Host zur Verfügung gestellt wird.
Wichtiger sind verschiedene Konfigurationsdateien, die Trino mit Hilfe eines Docker Volumes zur Verfügung gestellt werden. Die Konfigurationsdateien werden durch das Skript files/init/prep_trino.sh mit Hilfe der Informationen aus etc/iceduck generiert und landen dann an folgender Stelle :
- files/data/trino/etc/config.properties konfiguriert den Trino node zu einem Koordinator, Worker etc. und enthält auch die URLs und Ports.
- files/data/trino/etc/node.properties enthält weitere Node Eigenschaften
- files/data/trino/etc/jvm.config ist für die Konfiguration der Trino JavaVM notwendig.
- files/data/trino/etc/catalog/warehouse.properties enthält die Verbindungsdetails, die Trino braucht um sowohl den Polaris Katalog als auch den MinIO Storage ansprechen zu können. Das Template der Datei kann dem prep-Script entnommen werden.
- files/data/trino/etc/catalog/pgice.properties enthält die Verbindungsdetails, die Trino braucht um die Postgres Datenbank als eine weitere Quelle zu erkennen.
Sind die Konfigurationsdateien mit dem prep Skript erzeugt worden, kann Trino per docker compose down && docker compose up -d gestartet werden.
Im Stack ist ein weiteres Wrapperskript enthalten. bin/trino startet die Trino CLI direkt im Container mit den richtigen Parametern. Damit können trino Statements interaktiv verarbeitet werden.
Gleich mal ausprobieren, bin/trino :
trino> show catalogs;
Catalog
-----------
pgice
system
warehouse
(3 rows)
Query 20260121_164313_00001_n6cy3, FINISHED, 1 node
Splits: 7 total, 7 done (100.00%)
1.14 [0 rows, 150B] [0 rows/s, 132B/s]
Wow ! Das war nun die erste Interaktion mit meinem Data Lakehouse !
Spark
DataBricks stellt nach wie vor auf GitHub ein docker compose Script für Docker-Spark-Iceberg zur Verfügung. Enthalten ist neben PyIceberg, Spark und SparkSQL auch ein Jupyter Notebook Webserver, der es erlaubt, über eine Web-UI Kommandos abzusetzen. Das Setup ist charmant und einfach in unseren Stack zu integrieren. Da das originale Image aber versionstechnisch inzwischen etwas überholt ist, bauen wir uns mittels eine Dockerfiles das Image lokal nach und hängen das Ergebnis mit in unser bekanntes docker-compose.yaml
Auch hier gibt es wie bei Trino wieder die Herausforderung, die Verbindungsdetails zu unserer Minio/Iceberg/Polaris Installation in den Spark Container als Default Parameter zu konfigurieren. In iceduck init ist das bereits automatisiert, indem vor dem ersten Start des Spark Containers mit ./files/init/prep_spark.sh die entsprechenden Konfigurationsdateien erzeugt werden :
- files/data/spark/config/spark-defaults.conf enthält die Verbindungsdetails zu Polaris und Minio für Spark und SparkSQL
- files/data/spark/config/pyiceberg.yaml enthält die Verbindungsdetails zum Iceberg Cluster für die Verwendung mit PyIceberg
Läuft der Spark Container, haben wir verschiedene Möglichkeiten, Spark basierte Jobs auszuführen :
- Mit dem Jupyter Notebook unter http://localhost:8888 können ganze Python/PyIceberg/SparkSQL notebooks ausgeführt werden.
- PySpark/PyIceberg Programme können mit Hilfe des Wrapperscripts bin/pyspark auch über die Kommandozeile ausgeführt werden. Dafür startet das Wrapperscript eine entsprechende Shell mit einer vollständigen Konfiguration direkt im Container.
- SparkSQL Script können analog mit dem Wrapper bin/sparksql ausgeführt werden.
Mit Hilfe der Spark-UI lassen sich die Spark Jobs monitoren.
DuckDB
Die DuckDB ist kein permanent laufender Datenbankserver. Es wird bei Bedarf ein Container bzw. Prozess gestartet. Deshalb muss während der Vorbereitens mit iceduck init auch lediglich eine Workspace Verzeichnis angelegt, aber sonst nicht konfiguriert werden.
Der Softwarestack stellt ein Wrapperscript bin/duckdb zur Verfügung, welches einen Container mit einer DuckDB CLI erzeugt. Darin können dann beliebige SQLs audgeführt werden, auch der Connect zur Iceberg Instanz.
Ausblick
Der vorgestellte Stack bietet nun Möglichkeiten für umfangreiche Experimente mit einem Iceberg Stack. Geplant habe ich folgende Gebiete :
- die Funktionsweise von Apache Iceberg ( TimeTravel, Compaction, Partitioning ) untersuchen
- Unterschiede Trino/DuckDB/SparkSQL als Zugriffswerkzeuge herausarbeiten
- PyIceberg mit PyArrow erforschen
- Möglichkeiten von ETL/ELT Tools ( Apache Hop, dbt, … ) in Kombination mit Apache Iceberg untersuchen
- Vergleich dieses Setups mit kommerziellen Anbietern wie DataBricks, Snowflake, Motherduck und weiteren untersuchen
Weitere Ergebnisse gibts dann in weiteren Posts !