Über unsMediaKontaktImpressum
Kevin Welter 17. Dezember 2024

Cloud-Monolith – Wie Serverless-Architektur in der Realität scheitert

In den letzten Jahren haben Serverless-Architekturen stark an Popularität gewonnen. Sie versprechen Entwicklern die komplette Entlastung von der Infrastrukturverwaltung und bieten theoretisch eine unbegrenzte Skalierbarkeit bei geringeren Kosten. Der Reiz liegt darin, sich voll und ganz auf die Geschäftslogik konzentrieren zu können, ohne sich um Server kümmern zu müssen. Diese verlockende Einfachheit sowie das Versprechen, mühelos skalieren zu können, machen Serverless zu einer beliebten Wahl. Dienste wie AWS Lambda, Google Cloud Functions oder Azure Functions ermöglichen es, Funktionen direkt in der Cloud auszuführen, ohne sich um den Betrieb oder die Verwaltung von Servern zu sorgen.

Allerdings ist das, was auf den ersten Blick wie die perfekte Lösung erscheint, in der Praxis oft nicht so makellos. Serverless kann schnell zu einem kostspieligen Irrweg werden, wenn man die Feinheiten übersieht.

Ein häufiges Problem bei schnell wachsenden Serverless-Systemen ist, dass sie zu schwer verständlichen, komplexen Strukturen mutieren. Diese Komplexität entsteht durch den ungeplanten Ausbau von Nanoservices, undurchdachte Datenflüsse und unklare Verantwortlichkeiten. Schnell entstehen schwer handhabbare Abhängigkeiten, die die Wartbarkeit und Erweiterbarkeit erheblich erschweren. Was anfangs flexibel und skalierbar erschien, entwickelt sich oft zu einem Bremsklotz für die Weiterentwicklung und Stabilität des Systems.

Paradoxerweise entstehen in Serverless-Architekturen manchmal Monolithen – ein Widerspruch, der in einem meiner Projekte deutlich wurde. Dem gegenüber stehen kleinteilige Nanoservices, bei denen Funktionalitäten getrennt wurden, die zusammengehören und dadurch Probleme hervorrufen, die nicht existieren müssten.

Wir begeben uns heute auf eine Reise durch verschiedene Architekturen, die ich in Projekten vorgefunden habe und erfahren, was wir daraus lernen können. Dabei werfen wir einen detaillierten Blick auf Konnektoren – jene unscheinbaren, aber essenziellen Bausteine, die dafür sorgen, dass Daten zwischen unterschiedlichen Systemen ausgetauscht werden. Konnektoren werden oft als einfache Vermittler gesehen: Sie holen Daten von System A und liefern sie an System B.

Der perfekte Use Case für einfache Serverless-Funktionen. Doch in der Realität entpuppen sie sich häufig als neuralgische Punkte, an denen viele Architekturen scheitern. Das liegt an drei Punkten:

  • Verborgene Komplexität
    Was zunächst als einfacher Datenfluss zwischen zwei Systemen erscheint, entwickelt sich oft zu einem komplizierten Netz von Abhängigkeiten. Ein Konnektor, der ursprünglich lediglich für den Transfer von Daten gedacht war, wächst mit der Zeit zu einem zentralen Bestandteil der gesamten Systemarchitektur heran. Jedes neue Feature und jede zusätzliche Anforderung erhöht die Last auf der ursprünglichen Architektur, die nun mehr Funktionen erfüllen muss als ursprünglich geplant.
  • Business-Logik im Konnektor
    Ein weiteres Problem tritt auf, wenn Business-Logik in den Konnektor ausgelagert wird, die eigentlich in den angebundenen Systemen selbst gelöst werden sollte. Häufig müssen Konnektoren die Schwächen der angebundenen Systeme ausgleichen. Dadurch verwischen die Grenzen zwischen Infrastruktur und Geschäftslogik, was die Wartbarkeit und Erweiterbarkeit weiter erschwert. In vielen Fällen werden Konnektoren zu einem Ort, an dem Probleme von Schnittstellen-Systemen „ausgebadet“ werden, was das gesamte System anfällig für Fehler macht.
  • Enterprise-Architektur-Probleme werden sichtbar
    Konnektoren decken oft systemische Probleme in der übergeordneten Unternehmensarchitektur auf. Ein Beispiel hierfür sind Datenflüsse, die im Kreis laufen oder redundante Prozesse erzeugen. Dies passiert häufig, wenn eine schlechte Datenflussplanung dazu führt, dass dieselben Informationen mehrfach durch verschiedene Systeme wandern, ohne dass dies einen Mehrwert bietet. Konnektoren machen diese Mängel sichtbar, da sie die Schnittstellen und Datenflüsse direkt verwalten. Statt die Probleme an der Wurzel zu beheben, bleibt der Konnektor als provisorische Lösung bestehen und muss die Unzulänglichkeiten der Enterprise-Architektur kompensieren. Langfristig führt dies zu einem ineffizienten und schwer zu kontrollierenden System, das an vielen Stellen seine eigentliche Aufgabe, nämlich die Unterstützung der Geschäftsprozesse, verfehlt.

Wie schlechte Architektur entsteht

Ein zentraler Grund für schlechte Architektur ist das Fehlen regelmäßiger Refactorings. Refactoring bedeutet, Code kontinuierlich zu überarbeiten und zu optimieren, ohne dabei das äußere Verhalten der Software zu verändern. Ohne diesen Prozess sammeln sich mit der Zeit sogenannte technische Schulden an: Der Code wird zunehmend komplex, schwer verständlich und fehleranfällig. Kleine Workarounds und kurzfristige Lösungen häufen sich, was letztlich zu einem unübersichtlichen und instabilen System führt.

Warum passiert das?

  • Zeit- und Kostendruck: In vielen Projekten liegt der Fokus auf schnellen, kurzfristigen Lösungen, um Deadlines einzuhalten. Langfristige Maßnahmen wie Refactoring werden als „später zu lösen“ betrachtet – ein Zeitpunkt, der häufig nie kommt.
  • Fehlende Architekturvision: Ohne eine klare Strategie für die Systemarchitektur werden Entscheidungen oft ad hoc und situationsbedingt getroffen. Dies führt zu inkonsistentem Design und einer schwer wartbaren Struktur.
  • Unzureichende Ressourcenplanung: Refactoring wird häufig nicht als fester Bestandteil des Entwicklungszyklus eingeplant. Entwickler haben dadurch keine Zeit, bestehenden Code zu verbessern und konzentrieren sich ausschließlich auf neue Features.

Keine systemübergreifende Konsistenz

Ein weiteres Symptom schlechter Architektur ist das Fehlen einer systemübergreifenden Konsistenz. Dies tritt auf, wenn verschiedene Systeme oder Komponenten nicht harmonisch zusammenarbeiten und unterschiedliche Standards, Datenformate oder Schnittstellen verwenden.

  • Unterschiedliche Technologien und Frameworks: Wenn mehrere Teams an einem Projekt arbeiten, kann es vorkommen, dass sie unterschiedliche Technologien verwenden. Ohne eine übergeordnete Richtlinie für die Architektur führt dies zu Problemen bei der Integration.
  • Inkonsistente Schnittstellen: Wenn API-Designs und Datenformate nicht einheitlich gestaltet werden, entstehen Schwierigkeiten bei der Kommunikation zwischen den Komponenten. Jeder Dienst hat eigene Regeln, was zu einem schwer zu wartenden System führt.
  • Fehlende Standardisierung: Systeme entwickeln sich zu einer "Patchwork"-Lösung, wenn es keine Standards gibt, die alle Teams einhalten müssen. Änderungen in einem System haben dann unvorhersehbare Auswirkungen auf andere.

Fazit: Die langfristigen Folgen

Fehlendes Refactoring und mangelnde Konsistenz führen dazu, dass ein System unübersichtlich, ineffizient und anfällig für Fehler wird. Die Architektur leidet darunter, dass Komponenten nicht klar definiert oder sinnvoll entkoppelt sind. Dies erschwert die Wartung, erschwert Erweiterungen und führt letztlich dazu, dass das System teuer und riskant zu betreiben ist. Regelmäßiges Refactoring und eine klare, systemübergreifende Architekturstrategie sind entscheidend, um diese Probleme zu vermeiden. Doch wie können solche Schwächen erkannt und behoben werden?

Das Nanoservice-Beispiel

Werfen wir einen Blick auf ein konkretes Beispiel. In diesem Fall geht es um einen Konnektor, der durch viele kleine Services auf Basis von AWS-Lambda-Zeitreihen zwischen zwei Systemen austauscht. Wir wollen uns ansehen, wieso das in diesem Fall zu kleinteilig gewählt wurde und welche Probleme daraus entstehen.

IST-Architektur

  • AWS Lambda (Java)
    • Lambda-Funktion als zentrale Verarbeitungseinheit für die Datentransformation und den Datenfluss zwischen Systemen
  • Feed-Service
    • ruft minütlich verschiedene Zeitreihen-Daten von einem externen System ab (Polling-Mechanismus)
    • führt die Abfragen regelmäßig aus, um aktuelle Daten zu erhalten.
  • DynamoDB
    • Cache-Funktion: verwendet, um zu prüfen, ob dieselben Daten bereits verarbeitet wurden
    • Queue-Funktion: nutzt DynamoDB als Zwischenablage für neue Daten, um sie an nachgelagerte Systeme weiterzuleiten
  • Publisher-Service
    • reagiert auf Events, die von DynamoDB ausgelöst werden, wenn neue Daten vorliegen
    • transformiert die Daten in das gewünschte Format
    • sendet die transformierten Daten an das Zielsystem

Auf den ersten Blick wirkt diese Architektur wie eine typische Umsetzung, die den Grundsätzen der Separation of Concerns (SoC) folgt. Ein genauerer Blick offenbart jedoch folgende Schwächen:

Hohe Kosten durch DynamoDB

  • Problem: Die Nutzung von DynamoDB als Cache und Queue führte zu erheblichen Kosten durch zahlreiche Lese- und Schreibvorgänge, besonders bei großen Datenmengen.
  • Auswirkung: Die Kosten stiegen mit wachsender Datenmenge, wodurch die Lösung auf lange Sicht unwirtschaftlich wurde.

Limitierung der Objektgröße in DynamoDB

  • Problem: DynamoDB schränkt die Objektgröße ein, was bei großen Zeitreihen-Daten zu erheblichen Engpässen führte.
  • Auswirkung: Es kam zu Datenverlusten und verlangsamter Verarbeitung, da das System nicht in der Lage war, große Datensätze effizient zu verarbeiten.

Hohe Anfragerate in System A durch unabhängige Lambda-Funktionen

  • Problem: Für jede Art von Zeitreihe wurde eine separate Lambda-Funktion verwendet, die minütlich Daten abrief, was zu hoher Belastung auf der Schnittstelle führte.
  • Auswirkung: Die Schnittstellenlast war schwer zu begrenzen, ohne Verzögerungen bei der Datenverarbeitung zu verursachen, da die Daten sofort verarbeitet werden mussten.

Solche Probleme bleiben oft in der Planungsphase unbemerkt, sollten jedoch spätestens während der Implementierung erkannt und durch gezieltes Refactoring behoben werden.

Herangehensweise an Redesign

In solchen Fällen ist es wichtig zu überlegen, wieso diese Probleme auftauchen und wie diese gelöst werden können.
 
Problematische Aufteilung in Nanoservices

  • Zu kleinteilige Services: Die Architektur wurde in sehr kleine, spezialisierte Services aufgeteilt. Diese Nanoservices führen zu einer hohen Anzahl von unabhängigen Lambda-Funktionen, die jeweils nur für eine spezielle Aufgabe zuständig sind.
  • Negative Auswirkungen: Die Vielzahl an einzelnen Services führt zu erhöhter Komplexität, erhöhtem Verwaltungsaufwand und redundanten Prozessen. Statt einer effizienten Nutzung werden Ressourcen für Verwaltung und Orchestrierung aufgewendet.

Einsatz von DynamoDB zur Trennung von Services

  • DynamoDB als ungeeigneter Vermittler: DynamoDB wurde als Cache und Queue zwischen den Services verwendet, obwohl es eigentlich keine zentrale Rolle in der Architektur spielen sollte.
  • Problematik: Der Einsatz von DynamoDB an dieser Stelle verursacht hohe Kosten und unnötige Abhängigkeiten. DynamoDB führt zudem zu erhöhten Latenzen und bringt Limitierungen, die die Effizienz und Flexibilität des Systems beeinträchtigen.

Zusammenfassend zeigt sich, dass der übermäßige Einsatz von Nanoservices und die ungeeignete Verwendung von DynamoDB sowohl die Wartbarkeit erschwert als auch die Betriebskosten erhöht haben. Hier wurde unnötig getrennt, was fachlich zusammengehört. Die Analyse machte deutlich, dass eine restrukturierte, lose gekoppelte Architektur mit klar definierten Verantwortlichkeiten erforderlich ist – jedoch ohne zwingende physische Trennung.

In der überarbeiteten Architektur wurde der Connector durch einen konsolidierten Microservice in einem ECS Container ersetzt, um die vorherige Fragmentierung und die damit verbundenen Probleme zu beheben.
Ziel des Redesigns: "Wir bringen zusammen, was zusammen gehört." Die zuvor in mehrere Nanoservices aufgeteilten Funktionen wurden zu einem einzigen Microservice zusammengeführt, der alle notwendigen Aufgaben zentral durchführt.

ECS Container als zentraler Microservice:

  • Der gesamte Connector läuft nun in einem ECS Container (Elastic Container Service), der die Daten von System A abruft, verarbeitet und an System B weiterleitet.
  • Durch die Zentralisierung im ECS Container werden die zuvor verteilten Funktionen (Polling und Publishing) zusammengefasst, was eine bessere Effizienz und Kontrolle über die Datenflüsse ermöglicht.

Vorteile des Redesigns:

  • Reduzierte Abhängigkeiten und niedrigere Kosten: Die Notwendigkeit, DynamoDB als Queue und Cache zu verwenden, entfällt. Dies senkt die laufenden Kosten und verringert die Komplexität.
  • Bessere Wartbarkeit und Skalierbarkeit: Durch die Zusammenführung der Funktionen in einem Microservice ist das System weniger fragmentiert und einfacher zu warten. Bei Bedarf kann der ECS Container skaliert werden, ohne dass mehrere Lambda-Funktionen orchestriert werden müssen.
  • Vermeidung von Nanoservice-Overhead: Anstatt viele kleine Lambda-Funktionen für spezifische Aufgaben zu verwenden, bietet der Microservice eine klarere, kohärentere Lösung mit geringerer Latenz und einem geringeren Verwaltungsaufwand.

Die Software-Architektur im Detail

Feed-Komponente:

  • Funktion: Die Feed-Komponente übernimmt das regelmäßige Abrufen der Daten aus System A. Sie fungiert als Einstiegspunkt, der die Rohdaten sammelt und sie für die weitere Verarbeitung vorbereitet.
  • Keine Trennung der Zeitreihen. Alle werden als Paket abgeholt (da sie fachlich auch zusammengehören).
  • Queue: Nach der Erfassung der Daten werden diese in eine interne Queue (z. B. eine In-Memory-Queue) gestellt. Diese Queue sorgt für eine entkoppelte Verarbeitung der Daten in der nächsten Phase.

Transformer-Komponente:

  • Funktion: Der Transformer transformiert die Daten, um sie für das Zielsystem B kompatibel zu machen. Hier wird die eigentliche Datenlogik angewendet, etwa durch Datenformatumwandlungen, Aggregationen oder Filterungen.
  • In-Memory-Cache: Der Transformer verwendet einen  In-Memory-Cache, um Daten zu speichern, die für die Transformation relevant sind, z. B. zur Vermeidung von doppelter Verarbeitung oder um Ergebnisse temporär vorzuhalten. Dieser Cache ersetzt die vorherige DynamoDB-basierte Cache-Lösung, wodurch die Kosten und Latenzzeiten reduziert werden.

Sender-Komponente:

  • Funktion: Die Sender-Komponente ist verantwortlich für das Versenden der transformierten Daten an System B. Sie wartet auf die Daten aus der zweiten Queue und gewährleistet, dass die Daten sicher und in der richtigen Reihenfolge bei System B ankommen.
  • Queue: Die Queue zwischen Transformer und Sender dient zur weiteren Entkopplung und sorgt dafür, dass der Sender asynchron arbeiten kann, wodurch die Systemstabilität erhöht wird.

Kostenvergleich

Die Auswirkungen des Redesigns spiegeln sich deutlich in den Cloud-Kosten wider und verdeutlichen, warum die Umstrukturierung nicht nur technisch, sondern auch wirtschaftlich notwendig war. Vor der Anpassung führten die fragmentierte Architektur und der ineffiziente Einsatz von Ressourcen, wie DynamoDB als Cache und Queue, zu einer enormen Kostenexplosion. Jeder zusätzliche Schreib- oder Lesevorgang trieb die Betriebskosten in die Höhe, insbesondere bei wachsendem Datenvolumen.

Durch das Redesign konnte dieser Kostentreiber erheblich reduziert werden. Der Wechsel zu einem zentralisierten Microservice und die Nutzung effizienter Datenstrukturen eliminierten viele der vorherigen Engpässe. Kostenintensive Workarounds, wie das Skalieren einzelner Nanoservices oder unnötig häufige Datenabfragen, wurden überflüssig. Die optimierte Architektur reduzierte nicht nur die direkten Betriebskosten, sondern schuf auch langfristig eine stabilere und skalierbare Grundlage, die zukünftige Erweiterungen und Anpassungen erleichtert, ohne die Kosten erneut in die Höhe zu treiben.

Diese Optimierungen zeigen exemplarisch, warum technische Entscheidungen nicht isoliert betrachtet werden sollten, sondern immer auch im Kontext der Wirtschaftlichkeit und Skalierbarkeit eines Systems stehen müssen.

Das Cloud-Monolith-Beispiel

Betrachten wir nun das zweite Beispiel – das genaue Gegenteil des vorherigen. Während im ersten Fall eine übermäßige Fragmentierung das Problem darstellte, wurde hier auf eine Trennung vollständig verzichtet.

Die Struktur ist im Kern simpel: Eine zentrale AWS Lambda-Funktion übernimmt sämtliche Aufgaben. Sie empfängt die Daten, transformiert sie entsprechend den Anforderungen des Zielsystems und leitet sie weiter. Dabei erfüllt diese Lambda-Funktion folgende Aufgaben:

  • Herkunft der Daten: Identifizierung der Quelle
  • Ziel der Daten: Bestimmung, wohin die Daten gesendet werden sollen
  • Datenformat: Anpassung an das erforderliche Format.

Zusätzlich wird in der Lambda-Funktion Business-Logik implementiert, die bestimmte Daten filtert oder modifiziert. Diese zentrale Ansammlung von Verantwortlichkeiten führt zu einer starken Kopplung und einer hohen Komplexität, die wir im weiteren Verlauf genauer betrachten werden.

Ist-Architektur Cloud-Monolith

Das klingt zunächst praktikabel, doch die Realität dieser Architektur zeigt ihre Schwächen. Im Kern handelt es sich um eine einzige große Lambda-Funktion, die ihre Verarbeitung stark auf externe Konfigurationen in DynamoDB stützt. Eine Tabelle mit 89 verschiedenen Message Types diente als Grundlage für die Steuerung.

  • Zentrale Verarbeitungseinheit: Die Lambda-Funktion übernimmt sowohl den Empfang der Daten als auch deren Transformation. Dabei orientiert sie sich an externen Konfigurationsdaten, um die spezifische Verarbeitung durchzuführen.
  • Konfigurationszugriff: Um die Datenverarbeitung dynamisch anzupassen, greift die Lambda-Funktion bei jeder Ausführung auf die in DynamoDB gespeicherten Konfigurationsdaten und Nachrichtentypen zu.

Diese starke Abhängigkeit von externen Konfigurationen führt zu einer hohen Komplexität und macht die Architektur anfällig für Fehler und schwer wartbar. Im nächsten Schritt beleuchten wir die Herausforderungen, die sich aus diesem Ansatz ergeben.

Problemstellung

Die aktuelle Architektur nutzt rekursive Lambda-Aufrufe zur Verarbeitung und Aufteilung von Daten. Dies führt zu einer Kette von automatischen Aufrufen, die das System unnötig komplex und schwer kontrollierbar macht.

Rekursive Datenverarbeitung

  • Selbstaufruf: Die Lambda-Funktion ruft sich selbst erneut auf, indem sie Daten in einen Inbound-S3-Bucket schreibt. Dies löst ein neues Event aus, das die Lambda-Funktion wieder aktiviert.
  • Mehrstufige Prozesse: Jeder Durchlauf umfasst mehrere Schritte, darunter Identifizierung, Aufteilung in Streams, Filterung und Transformation der Daten.
  • Kleine Verarbeitungseinheiten: Für jedes neue Datenpaket wird die Funktion erneut gestartet, was zu zahlreichen kleinen Verarbeitungsschritten führt.

Herausforderungen

  • Komplexität und Kosten: Die ständigen Selbstaufrufe führen zu hohen Betriebskosten, da für jeden Schritt eine neue Lambda-Initiierung erforderlich ist. Zudem wird der Code der Lambda immer wieder durch die Message Types geleitet, um den nächsten Prozessschritt zu ermitteln.
  • Schwierige Überwachung: Der Datenfluss wird durch die rekursive Verarbeitung extrem komplex. Fehler und Engpässe sind schwer zu identifizieren, was die Fehlersuche erheblich erschwert.
  • Erhöhte Latenz: Jeder Aufruf erzeugt zusätzliche Verzögerungen, da die Funktion bei jeder Ausführung neu initialisiert und ausgeführt wird. Dies verlängert die Gesamtverarbeitungszeit erheblich.

Dynamische Konfigurationen in DynamoDB

Die Konfigurationen und Nachrichtentypen, die die Lambda-Funktion steuern, werden in DynamoDB gespeichert. Das bedeutet, dass die Verarbeitung dynamisch von externen Daten abhängt, die bei jeder Ausführung neu eingelesen werden.

Herausforderungen

  • Schwierig zu testen: Die dynamische Abhängigkeit von DynamoDB erschwert die Erstellung isolierter Unit Tests. Da die Konfiguration aus einer externen Datenbank stammt, kann die Lambda-Funktion nicht unabhängig getestet werden.
  • Message Types enthalten Filterlogik: Es wurde versucht, so generisch wie möglich zu sein, was wiederum zu schwer verständlicher Logik führt.

Herangehensweise an Redesign

Es ist deutlich erkennbar, dass das Prinzip der Separation of Concerns (SoC) in dieser Architektur nicht umgesetzt wurde. Die fehlende Trennung der Verantwortlichkeiten hat zu einer übermäßigen Komplexität und einer schwer wartbaren Struktur geführt.

Herausforderungen durch hohe Komplexität:

  • Schwer zu verstehen: Mit 89 verschiedenen Message Types ist es schwierig, den Überblick zu behalten und zu wissen, welcher Konfigurationstyp welche Verarbeitungsschritte in der Lambda-Funktion beeinflusst.
  • Fehlende Übersichtlichkeit: Die Vielzahl an Konfigurationen macht es schwer, gezielt Anpassungen vorzunehmen oder die Auswirkungen einzelner Änderungen zu verstehen, was die Architektur fehleranfällig macht.
  • Schwieriges Debugging: Wenn Fehler auftreten, ist es oft unklar, welche Konfiguration oder welcher Message Type die Ursache ist. Das Debuggen wird zu einem langwierigen und komplizierten Prozess, da jede Konfiguration in DynamoDB geprüft werden muss.

In der aktuellen Monolith-Architektur führt die enge Kopplung der Datenstreams zu erheblichen Herausforderungen bei der Wartung und Erweiterung.

Rollout-Abhängigkeiten:

  • Eine Änderung oder Anpassung an einem einzelnen Datenstream erfordert einen vollständigen Rollout und Test der gesamten Architektur. Da alle Streams eng miteinander verbunden sind, können Änderungen an einem Stream unbeabsichtigte Auswirkungen auf andere haben.
  • Dies führt zu langen Release-Zyklen, da jeder Rollout umfangreiche Tests und Validierungen für alle Datenstreams umfasst, selbst wenn nur ein Stream tatsächlich modifiziert wurde.

Fehlende Modularität:

  • Die Monolith-Architektur erlaubt keine isolierte Bereitstellung von Updates für einzelne Datenstreams. Dies bedeutet, dass selbst kleine Anpassungen im gesamten System getestet werden müssen, was die Flexibilität und Agilität der Architektur einschränkt.
  • Jede Anpassung benötigt umfassende Regressionstests, um sicherzustellen, dass die Änderungen keine negativen Auswirkungen auf andere Teile des Systems haben.

Das bedeutet, das Ziel des Redesigns ist, die bestehende Monolith-Architektur in eine flexible, skalierbare und wartbare Struktur zu überführen, die eine einfachere Anpassung und Bereitstellung einzelner Datenstreams ermöglicht.
 
1. Aufbrechen in einzelne Datenstreams

  • Unabhängigkeit: Jeder Datenstream wird als eigenständige Komponente behandelt, wodurch Anpassungen, Bereitstellungen und Skalierungen unabhängig voneinander möglich werden.
  • Entkopplung: Die Trennung der Streams ermöglicht es, Änderungen oder Updates an einem Stream vorzunehmen, ohne dabei andere Streams zu beeinflussen. Dies reduziert potenzielle Risiken und erhöht die Flexibilität. 

2. Trennung der Logik in einzelne Services, wo notwendig

  • Modularisierung: Geschäftslogik wird in separate Services aufgeteilt, um die Komplexität zu verringern und die Wiederverwendbarkeit zu fördern.
  • Spezifische Services: Datenverarbeitungsschritte, die ausschließlich für bestimmte Datenstreams relevant sind, werden in dedizierten Services implementiert. Dies schafft klare Verantwortlichkeiten und verbessert die Wartbarkeit.

3. Berücksichtigung von Separation of Concerns (SoC)

  • Klar definierte Rollen: Jeder Service und jede Komponente übernimmt eine klar definierte Aufgabe und ist ausschließlich für einen bestimmten Aspekt der Datenverarbeitung verantwortlich.
  • Reduzierte Abhängigkeiten: Die Trennung der Verantwortlichkeiten sorgt dafür, dass Änderungen in einem Teil des Systems keine unbeabsichtigten Auswirkungen auf andere Bereiche haben.

4. Einhaltung des Single Responsibility Principle (SRP) aus SOLID

  • Fokus auf eine Aufgabe: Jeder Service wird so gestaltet, dass er nur eine spezifische Aufgabe erfüllt. Dies minimiert Abhängigkeiten und vereinfacht sowohl das Testen als auch die Wartung.
  • Isolation von Änderungen: Durch die Einhaltung des SRP können Änderungen an der Logik eines Datenstreams isoliert durchgeführt werden, ohne negative Seiteneffekte auf andere Funktionen.

In diesem Fall ist ein strukturiertes und schrittweises Vorgehen entscheidend, da die Vielzahl an unterschiedlichen Datenstreams eine isolierte und nacheinander erfolgende Anpassung erfordert. Die folgende Methode bietet einen klaren Rahmen für die Umgestaltung:

1. Identifikation des bestehenden Codes

  • Ziel: Isolieren und analysieren Sie den bestehenden Code, um die relevanten Teile der alten Architektur zu identifizieren.
  • Maßnahmen: Dokumentieren Sie die aktuelle Struktur und Funktion der einzelnen Komponenten, um einen vollständigen Überblick über die Ausgangssituation zu erhalten. 

2. Analyse der Schnittstellen (Interfaces)

  • Ziel: Untersuchen Sie die aktuellen Schnittstellen, um deren Input- und Output-Anforderungen zu verstehen.
  • Maßnahmen: Analysieren Sie, welche Daten gesendet und empfangen werden, und bewerten Sie die bestehenden Abhängigkeiten zwischen den Streams.

3. Sicherstellung der Konsistenz von Input und Output

  • Ziel: Beibehalten der bestehenden Ein- und Ausgabestruktur, um die Integration der neuen Architektur in das Gesamtsystem zu gewährleisten
  • Maßnahmen: Dokumentieren Sie den aktuellen Zustand von Input und Output und stellen Sie sicher, dass dieser im neuen Design unverändert bleibt, um Kompatibilitätsprobleme zu vermeiden.

4. Validierung durch Unit Tests

  • Ziel: Sicherstellen, dass die neue Architektur dieselben Ergebnisse wie die alte liefert
  • Maßnahmen:
    • Schreiben Sie Unit Tests, die bestehende Produktionsdaten verwenden, um die Konsistenz der neuen Implementierung zu überprüfen.
    • Beispiel: Senden Sie ein JSON-File als Input an den neuen Connector und vergleichen Sie das Resultat mit dem erwarteten Output (z. B. XML).

Ein einzelner Prozess im Detail

Die aktuelle Lambda-Funktion weist eine hohe Komplexität auf, da sie mehrere Verantwortlichkeiten gleichzeitig erfüllt. Diese Überladung führt zu schwer verständlichem Code und erhöht die Fehlerrate.

Transformation Lambda:

  • Split-und-Filter-Logik: 24 verschiedene Message Types werden aufgeteilt und gefiltert, bevor sie in den Inbound Bucket gelangen.
  • Transformation je Nachricht und Zielsystem: 18 weitere Message Types, die transformiert werden, basierend auf Nachrichtentyp und Zielsystem
  • Sende-Logik je nach Zielsystem: Die transformierten Daten werden an die jeweiligen Systeme (System A, B oder C) weitergeleitet.
  • Ruft sich immer wieder selbst auf. Hohe Komplexität durch viele MessageTypes: Die Vielzahl an Message Types führt zu komplexem und schwer verständlichem Code.

Schon hier ist zu erkennen, wie kompliziert der Aufbau ist. Wir haben uns dazu entschieden, die Komponenten schrittweise zu entkoppeln.

Schritt 1

Die Lambda-Funktion fokussiert sich jetzt nur noch auf Split-, Filter- und Transformationsaufgaben. Die Sende-Logik wurde ausgelagert und wird nun von einem ECS-Container übernommen.

Vorteil:

  • Lastreduktion: Die Lambda-Funktion wird entlastet, was ihre Komplexität und Ausführungsdauer erheblich reduziert.
  • Spezialisierung: Jede Komponente hat klar definierte Verantwortlichkeiten, was die Wartbarkeit und Erweiterbarkeit erleichtert.

Entkopplung der Sende-Logik:

  • Vorher: Die Lambda-Funktion führte die gesamte Logik aus, einschließlich Split, Filter, Transformation und Versand an die Zielsysteme (System A, B, C).
  • Jetzt: Die Lambda-Funktion übernimmt nur noch die Split-, Filter- und Transformationslogik. Die Sende-Logik wird an den ECS-Container ausgelagert, was die Last auf die Lambda reduziert.

SQS-Queue als Puffer:

  • Neu hinzugefügt: Die SQS-Queue dient als Puffer zwischen der Transformation Lambda und dem Sender-Container, um die Datenübertragung zu entkoppeln. Dies erhöht die Zuverlässigkeit und Skalierbarkeit, da die Daten in der Queue zwischengespeichert und vom Sender asynchron abgearbeitet werden können.

Bessere Skalierbarkeit und Wartbarkeit:

  • Vorher: Die Lambda-Funktion war für alle Schritte zuständig, was sie komplex und schwer wartbar machte.
  • Jetzt: Die Entkopplung durch die Queue und die Verlagerung der Sendelogik in den ECS-Container reduziert die Komplexität der Lambda-Funktion und vereinfacht zukünftige Wartungen und Erweiterungen.

Schritt 2

Im zweiten Schritt sind wir von der anderen Seite herangegangen.

Einführung von Kafka:

  • Neu hinzugefügt: Kafka wird als verlässlicher Daten-Stream für die Aufnahme von Nachrichten eingeführt. Dies ermöglicht eine skalierbare und robuste Verarbeitung großer Datenmengen, die kontinuierlich in das System gelangen. Keine synchronen Aufrufe mehr.
  • Ein Consumer im ECS Container liest die Daten aus Kafka und übergibt sie an die nächste SQS-Queue zur weiteren Verarbeitung (SoC).

Aufteilung der Split-und-Filter-Logik:

  • Die Split-und-Filter-Logik wird jetzt von einer eigenen Lambda-Funktion ausgeführt, die die Daten aus der SQS-Queue nach dem Kafka-Consumer empfängt.
  • Dadurch wird die Verarbeitung der 24 verschiedenen Message Types klar getrennt und modularisiert, was die Wartbarkeit erhöht.

Transformation-Logik verbleibt erstmal:

  • Wieder ein kleiner überschaubarer Schritt nach dem anderen.
  • Dadurch sinkt das Risiko auf Produktion.

Finaler Schritt

Zusammenlegung von Transformation und Splitting:

  • Die Transformation und das Splitting der Daten wurden in eine gemeinsame Lambda-Funktion integriert. Dies vereinfacht den Datenfluss und reduziert die Anzahl der separaten Verarbeitungsschritte.
  • Vorteil: Durch die Zusammenführung wird die Architektur schlanker, da weniger separate Komponenten verwaltet werden müssen. Die Transformationslogik wurde vereinfacht, sodass die Funktion sowohl das Aufteilen der Daten als auch die Umwandlung in einem einzigen Schritt erledigen kann. Generell waren das einfache und zusammengehörige Prozesse, die nicht getrennt werden mussten.

Reduzierung der Zielsysteme:

  • System C liest jetzt direkt aus Kafka. Dadurch entfällt die Notwendigkeit, System C durch den Connector zu bedienen, was die Komplexität und Datenverarbeitungslast weiter reduziert.

Prüfung der Konsistenz von Input und Output:

  • Bei jeder Änderung wurde geprüft, dass Input und Output gleich bleiben.
  • Keine generischen Klassen, die durch DynamoDB-Konfigurationen angepasst werden, sorgen für bessere Unit und Integration Tests.

Der abschließende Schritt des Redesigns resultiert in einer deutlich vereinfachten Architektur. Die Zusammenführung der Logik und die Reduzierung der Zielsysteme sorgen für einen effizienteren, klareren und besser wartbaren Datenfluss. Zusätzlich wurden die fachlichen Anforderungen sorgfältig überprüft, um sicherzustellen, dass ausschließlich notwendige Datenverarbeitungsprozesse erhalten bleiben. Dies minimiert unnötige Komplexität und stärkt die langfristige Stabilität der Architektur.

Fazit

Im Kern basiert jedes erfolgreiche Redesign auf zwei essenziellen Elementen:

  1. Input: Was wird in das System eingespeist?
  2. Output: Was soll das System am Ende liefern?

Wenn diese beiden Faktoren klar definiert sind, können Sie die Architektur beliebig umstrukturieren. Entscheidend ist, dass für jeden Prozess ein klarer Input und ein erwarteter Output vorhanden sind. Diese Transparenz bildet die Grundlage für eine erfolgreiche Transformation.
 
Besonders bei komplexen Systemen wie einem Cloud Monolith stellt diese Klarheit eine Herausforderung dar. Während der Entwicklung zeigte sich, dass die Definition dieser Elemente nicht immer einfach ist und ohne klare Dokumentation oft mühsam durch Reverse Engineering ermittelt werden muss.
 
Empfehlung: Fachliche Beschreibungen der Anforderungen und Prozesse sind unerlässlich, um den Aufwand zu reduzieren und die Architektur effizient und zielgerichtet zu überarbeiten. Sie verhindern Missverständnisse und sorgen dafür, dass die neue Struktur optimal auf die tatsächlichen Bedürfnisse abgestimmt ist.

Autor

Kevin Welter

Als passionierter Entwickler mit einem tiefen Verständnis für Softwarearchitekturen und Unternehmensprozesse liebe ich es, diese nicht nur zu entwerfen, sondern auch direkt in Code umzusetzen.
>> Weiterlesen
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben