Open-Source-Lösungen werden aktuell immer beliebter für Datenarchitekturen. Sie zeichnen sich unter anderem durch ihre Flexibilität, Skalierbarkeit und geringen Kosten aus. So können sie auf spezifische Anwendungsfälle zugeschnitten werden und problemlos große Datenmengen verarbeiten. In diesem Blogbeitrag zeige ich, wie so eine Lösung aussehen kann.
Unser Beispielprojekt sieht so aus: Unsere Daten liegen in relationalen Datenbanken dezentral an diversen Zweigstellen in Deutschland, möglicherweise in variierenden Formaten. Wir wollen diese Daten zusammenführen und mit bereits zentral vorhandenen Daten verbinden um sie ganzheitlich nutzen, auswerten und vergleichen zu können. Gleichzeitig soll sich der Prozess für die Zweigstellen möglichst simpel gestalten.
Um unsere Daten zu sammeln, verwenden wir MinIO. MinIO ist ein objektbasiertes Speichersystem, welches unstrukturierte Daten speichern kann. Darunterfallen nicht nur Daten aus unterschiedlichen Quellen und von unterschiedlicher Qualität, wie in unserem Anwendungsbeispiel. Es können auch Fotos, Videos oder andere Formate, für die relationale Datenbanken nicht die beste Lösung sind, mit MinIO gespeichert werden. Flexibilität ist eine der großen Stärken von MinIO, es kann je nach Datenmenge auf unterschiedlichen lokalen Servern oder Serverpools, aber auch in der Cloud eingesetzt werden.
MinIO kann mehrere Laufwerke unterschiedlicher Quellen in einem Objektspeicherserver zusammenfassen. Durch diese Verteilung können die Daten auch beim Ausfall einzelner oder sogar mehrerer Laufwerke gesichert werden. Durch native Kubernetes- und Dockerunterstützung wohnt MinIO ein hohes Maß an Skalierbarkeit inne. Objekte können mit jeder Amazon S3-kompatibler Software auf ein MinIO-Deployment geladen werden, z.B. mit der Python-Bibliothek boto3 von AWS.
Hier geht's zu mehr Data Engineering:
Jetzt haben wir all unsere Daten gesammelt. So weit, so gut. Wir haben die Daten aus unterschiedlichen Quellen allerdings bisher nur als Objekte auf unserem MinIO-Server gespeichert. Der nächste Schritt ist nun, unsere Daten in einem einheitlichen Datenmodell zusammenzuführen und in einer relationalen Datenbank zu speichern, damit wir sie als Ganzes analysieren und nutzen können. Um das zu erreichen, verwenden wir die Programmiersprache Python und spezifisch die Bibliothek Prefect für unsere ETL-Strecken. Am Ende dieser ETL-Strecken sollen die Daten in einer PostgreSQL-Datenbank gespeichert werden.
Da unsere Datenobjekte auf dem MinIO-Server unterschiedlicher Form sind, werden wir auch eine Reihe unterschiedlicher ETL-Strecken bauen müssen, um sie alle passend zu verarbeiten. Gleichzeitig sollen neue Datenobjekte auf unserem MinIO-Server zeitnah verarbeitet und in unserer relationalen Datenbank gespeichert werden. Unsere ETL-Strecken haben wir nun in Python, die Ausführung in der korrekten Reihenfolge und zu geplanten Zeiten haben wir damit aber noch nicht. Das können wir mit Prefect erreichen. Prefect ist ein so genanntes Workflow Management Tool. Es ermöglicht uns diverse in Python geschriebene Datenstrecken unterschiedlichster Komplexität zusammenzusetzen und in einzelnen Jobs zu vereinen und anschließend die Ausführung zu planen und orchestrieren. Prefect überzeugt im Vergleich zu Airflow mit einer einfacheren Einbindung von Python Code. Der wird hier lediglich mithilfe von Dekorierern für Prefect verständlich gemacht, während bei Airflow eigene DAG-Operatoren verwendet werden müssen. Prefect speichert die Metadaten und Historie unserer ETL-Strecken in einer eigenen Datenbank und verfügt über ein browserbasiertes Dashboard, um diese Daten anzuzeigen und gegebenenfalls die Ausführung weiterer Strecken anzupassen sowie das Deployment zu steuern.
Nachdem unsere Daten die ETL-Strecken erfolgreich durchlaufen haben, werden sie in unserer PostgreSQL-Datenbank gespeichert. Postgres hat einige interessante Eigenschaften. So ist es keine rein relationale Datenbank und bietet auch die Möglichkeit Objekte zu speichern. Python kann in PostgreSQL als sogenannte Procedural Language verwendet werden, um Funktionen für die Datenbank zu schreiben. Diese Stored Procedures werden auf der Postgres-Datenbank ausgeführt. Dies erleichtert die Verarbeitung von Datenströmen aus Pythonanwendungen (wie unseren ETL-Strecken), da die Umwandlung von Pythondatentypen hin zu Datenbankdatentypen genau bestimmt werden kann.
Den Pythoncode für unsere ETL-Strecken hosten wir in einem GitLab-Repository. GitLab basiert auf Git und hat somit alle Vorteile, die Git mit sich bringt: Durch die verteilte Versionsverwaltung können Entwickler*innen parallel an den gleichen Dateien arbeiten. Etwaige Konflikte in der Historie zweier Branches werden bei der Zusammenführung (merge) gelöst.
GitLab enthält neben Git aber noch weitere Funktionen. Für uns ist vor allem die Continuous Integration (CI) interessant. Unser Git-Workflow umfasst vier Branchtypen: Lokale Branches für die Entwicklung neuer Features, einen Development-Branch, einen Release-Branch und einen Production-Branch.
Die CI-Pipeline greift, sobald ein neues Feature aus einem lokalen Branch in den Development-Branch gemerged werden soll. Der Code durchläuft nun automatisch eine Reihe automatisierter Tests. So stellen wir die Qualität jedes einzelnen Beitrags sicher. Hat der Code die Tests erfolgreich durchlaufen, wird er via Pull Request in den Release-Branch eingefügt und in unserer Testumgebung deployed. Läuft auch hier alles rund, ist der Weg frei, um das neue Feature wieder mittels Pull Request in den Production-Branch einzufügen und kann dann auf unserem Prefect-Server in die Anwendung gehen.
MinIO: https://min.io/docs/minio/kubernetes/upstream/
Prefect: https://docs.prefect.io/latest/
GitLab: https://docs.gitlab.com/
PostgreSQL: https://www.postgresql.org/docs/