Wie stelle ich die Datenkonsistenz in einer eventbasierten Microservice-Architektur sicher?
Für uns ist Feedback eine essenzielle Form des Lernens. Dafür nutzen wir bei ITGAIN unsere hauseigene Cloud-Native-Anwendung elVira (elektronische Vortragsinformation, -resonanz, -auswertung). Mit Hilfe von elVira können ITGAIN-Kolleg:innen ihr Feedback zu Vorträgen oder Workshops abgeben.
elVira ist eine Cloud-Native-Anwendung, welche nach dem Domain-driven Design modelliert und als eine eventbasierte Microservice-Architektur umgesetzt wurde. Innerhalb dieser eventbasierten Microservice-Architektur kommunizieren Microservices, um Domain-Events über Nachrichten zu propagieren oder Daten auszutauschen. Wie also gewährleisten wir, dass Daten und Nachrichten sicher und konsistent übertragen werden? Das Ganze veranschaulicht am Beispiel elVira und mit Hilfe des Outbox-Patterns erklärt in diesem Beitrag.
Eventbasierte Microservices-Architektur
Wir haben die Microservices als Spring-Boot-Anwendungen implementiert, welche innerhalb eines OpenShift-Kubernetes-Clusters betrieben werden (s. Abb. 1). Jeder Microservice besitzt eine eigene PostgreSQL-Datenbank. Für die sichere und fehlertolerante Kommunikation, Verwaltung und das Traffic-Management der Microservices wird Istio als Service-Mesh-Framework eingesetzt. Services kommunizieren untereinander lose gekoppelt über Kafka-Nachrichten. Zur Vereinfachung betrachten wir im Weiteren nur die kafka-nachrichtenbasierte Kommunikation.
Nehmen wir an, dass für das Feedback-Aggregat ein Update vorgenommen wurde. Dieses Domain-Event wird mit Hilfe von Kafka-Nachrichten an andere Microservices propagiert. Hierbei werden die Kafka-Nachrichten in Kafka-Topics geschrieben. An diese Daten interessierte Microservices können solche Kafka-Nachrichten wieder aus den Kafka-Topics auslesen. Die Kommunikation läuft mit Hilfe von Kafka bei gleichzeitiger Sicherstellung der Einhaltung der Nachrichtenreihenfolge asynchron ab. Wichtig ist hierbei die Sicherstellung der Datenkonsistenz über Servicegrenzen hinweg. Wie sehen diese Anforderungen im Detail aus?
Problemstellung
Im Admin-Microservice können Vorträge oder Workshops anhand der Bewertungen aus den abgegebenen Feedbacks ausgewertet werden. Da die Feedbacks nur innerhalb der Datenbank des Feedback-Microservices liegen, müssen die Bewertungen an den Admin-Microservice übertragen werden.
Angenommen, der Feedback-Microservice propagiert Updates zu Feedbacks an den Admin-Microservice, dargestellt in Abb. 2: Dann werden im Speziellen die aktuellen Bewertungen aus einem Feedback über Kafka-Nachrichten in ein separates Kafka-Topic geschrieben. Im Admin-Microservice werden anschließend diese Daten aus dem Topic konsumiert. Um dies zu bewerkstelligen, wird innerhalb einer lokalen Transaktion im Feedback-Microservice das Update durchgeführt, indem die neuen Daten in die lokale Feedback-Datenbank festgeschrieben werden und anschließend eine Kafka-Nachricht erstellt und an das entsprechende Topic versendet wird.
Leider ist damit allein nicht einmal eine eventuelle Datenkonsistenz sichergestellt. Durch die lokale Transaktion wird sicher in die lokale Feedback-Datenbank geschrieben und die Datenkonsistenz im Feedback-Microservice ist damit sichergestellt. Das Schreiben in die Datenbank und das Propagieren eines Domain-Events über eine Kafka-Nachricht stellt eine weitere Transaktion dar. Wie in Abb. 3. zu sehen, kann es passieren, dass z. B. wegen eines Ausfalls die Kafka-Nachricht nie im entsprechenden Kafka-Topic ankommt. Das Update wird somit verlorengehen und kommt damit niemals beim Admin-Microservice an. Kann das Outbox-Pattern dabei helfen?
Outbox-Pattern zur Sicherstellung einer eventuellen Datenkonsistenz
Um über Microservice-Grenzen hinweg eine eventuelle Datenkonsistenz sicherzustellen, kann das Outbox-Pattern verwendet werden. Hierbei werden alle zu propagierenden Änderungen an einem DDD-Aggregat innerhalb einer lokalen Transaktion zusätzlich in eine separate Datenbanktabelle – die Outbox-Tabelle – geschrieben. Bei einem Feedback-Update wird damit innerhalb einer lokalen Transaktion in die lokale Datenbank (Feedback-DB) sicher geschrieben.
Des Weiteren wird mit Hilfe einer Change-Data-Capture-Implementierung (CDC) die Kommunikation Richtung Kafka vorgenommen. In elVira nutzen wir dafür Debezium, mit dessen Hilfe und der CDC werden Änderungen der Outbox-Tabelle aufgezeichnet und in einen separaten Log geschrieben, die anschließend an ein Kafka-Topic mit Hilfe des Kafka-Connect-Frameworks versendet werden. Die Kafka-Connect-Laufzeitumgebung ermöglicht es, Debezium mit Kafka zu verbinden.
Wenn beim Versuch, die Kafka-Nachrichten zu versenden, ein Fehler eintritt, z. B. durch einen Crash, wird nach einem Neustart des Microservices die Kafka-Nachricht, welche nicht an das Topic übermittelt werden konnte, wiederholt versendet. Somit wird mit Hilfe des Outbox-Patterns und der Debezium-CDC sichergestellt, dass Domain-Events über Kafka sicher propagiert werden.
Inbox-Pattern zur Vermeidung einer Mehrfachverarbeitung von Kafka-Nachrichten
Wenn bspw. nach einem Crash der Feedback-Service neu startet, werden durch Debezium eventuell bereits versendete, aber nicht bestätigte, Kafka-Nachrichten erneut versendet. Auf Kafka-Consumer-Seite muss von daher die Implementierung im Admin-Microservice idempotent sein, indem bspw. eine Nachrichten-ID für bereits verarbeitete Kafka-Nachrichten mitgespeichert wird. Dies kann analog zur Outbox-Tabelle innerhalb einer Inbox-Tabelle geschehen. Dadurch, dass vor einem Update des Aggregates geprüft wird, ob eine empfangene Kafka-Nachrichten bereits verarbeitet wurde, wird eine mehrfache Verarbeitung verhindert.
Implementierung Transactional Outbox in OpenShift
Für elVira setzen wir Strimzi – zusammen mit Kafka Connect – als Operator innerhalb unseres OpenShift-Kubernetes-Clusters ein (s. Abb. 4). Mit Hilfe von Strimzi wird ein Kafka-Cluster samt Topics innerhalb von OpenShift bereitgestellt.
Für die CDC-Verarbeitung wird zusätzlich mit Hilfe von Kafka Connect ein Debezium Postgres Connector eingesetzt. Dadurch werden die aufgezeichneten Änderungen innerhalb der Outbox-Tabelle im Kubernetes-Cluster nach Kafka übertragen. Des Weiteren können mit Hilfe einer Custom Debezium Single-Message-Transform-Implementierung (SMT) Nachrichten vor dem Kafka-Versand verarbeitet werden. Für die Implementierung einer Inbox-Tabelle kann hierfür innerhalb der SMT eine ID mit übergeben werden.
Mehr zum Thema Cloud-Native-Softwareentwicklung mit Microservices und Domain-driven Design erfahren Sie beim ITGAIN Cloud-Native-Tag am 22. März. Jetzt anmelden!
Fazit
elVira wurde nach dem Domain-driven-Design-Vorgehen als eine eventbasierte Microservice-Anwendung umgesetzt. Wir haben uns für eine eventuelle Datenkonsistenz entschieden, da für uns eine schnelle Datenverarbeitung mit geringer Latenz Vorrang hat.
Durch den Einsatz des Outbox-Patterns konnte ein Teil des Quellcodes der ursprünglichen elVira-Version, welche als Monolith implementiert wurde, übernommen werden. Eine alternative Lösung wäre bspw. der Einsatz von Event Sourcing und Command Query Responsibility Segregation (CQRS). Allerdings müsste hier ein Großteil des Codes umgeschrieben werden und ein größeres Refactoring (re-architect) wäre nötig. Da eine strikte Datenkonsistenz nicht benötigt wird und die Performance der Datenverarbeitung im Vordergrund stand, kam während der Umsetzung der Einsatz globaler Transaktionen (two-phase commit) nicht in Frage.
Ein Nachteil des Outbox-Patterns ist, dass Daten, welche über Servicegrenzen hinweg benötigt werden, immer in die Outbox-Tabelle geschrieben werden müssen. Hier kann es passieren, dass dieser weitere Schritt innerhalb der Entwicklung übersehen wird.
Insgesamt überwiegen die positiven Eigenschaften des Outbox-Patterns, welche wir während der Entwicklung sammeln konnten. Nach Abwägung verschiedener Ansätze war dies für uns die einfachste und effektivste Lösung.