Monolith vs. Microservices Part 9: Die Gretchenfrage – Konsistenz oder Verfügbarkeit?
BettercallPaul
BettercallPaul

// //
Lesedauer: 7 Minuten

Monolith vs. Microservices Part 9

die Gretchenfrage – Konsistenz oder Verfügbarkeit?


Datenhaltung und verteilte Transaktionen sind herausfordernde Themen im Kontext von verteilten Systemen. In verteilten Systemen gilt zudem das CAP-Theorem, das besagt, dass nur zwei der drei Eigenschaften Konsistenz, Verfügbarkeit und Partitionstoleranz erfüllt werden können, wobei Partitionstoleranz gesetzt ist. Die Entscheidung fällt also zwischen Verfügbarkeit und Konsistenz. Wie entscheide ich für mein Projekt, welche Eigenschaft ich priorisiere?

Zusammenfassung

In vielen verteilten Systemen wird auf BASE gesetzt, also auf schlussendliche Konsistenz zugunsten von hoher Verfügbarkeit. Sollte aufgrund der kurzen Zeit der Inkonsistenz ein Fehler passieren, zum Beispiel ein Menü bestellt werden, das nicht mehr verfügbar ist, müssen solche Fehler natürlich dediziert behandelt werden. In unserem Beispiel würde Moni den Kund:innen Gutscheine ausgeben, wenn Bestellungen nicht erfüllt werden können.

Gutscheine auszugeben, wenn Bestellungen nicht erfüllt werden können, ist aber nicht für alle Systeme die Lösung. Kritischere Anwendungen (zum Beispiel zum Buchen von Flügen) müssen anders mit dem CAP-Theorem umgehen. In solchen Anwendungen ist es oft so, dass der Bestellvorgang (Benutzer:in fügt etwas zum Warenkorb hinzu) entkoppelt vom tatsächlichen Bestellen ist. Beim Drücken auf Bestellen wird die Bestellung gegen den tatsächlichen Bestand, der in einen weiteren Service ausgelagert ist, abgeglichen und erst wenn dieser Service sein OK gibt (zum Beispiel: Platz noch frei), dann kann bestellt werden. In diesen Anwendungen wird also je nach fachlicher Kritikalität zwischen der Kante AP und CP des CAP-Theorems unterschieden:

  • Benutzer können ihren Warenkorb füllen – unabhängig vom Lagerbestandsservice, weil die Daten in den Service zum Füllen des Warenkorbs repliziert wurden, das heißt der Warenkorb ist unabhängig vom Lagerbestandsservice verfügbar, aber dafür nicht zu jedem Zeitpunkt konsistent mit dem Lagerbestand (Kante AP).
  • Beim tatsächlichen Bestellvorgang wird der Lagerbestandsservice aufgerufen. Wenn dieser nicht verfügbar ist, ist keine Bestellung möglich. Dafür ist Konsistenz sichergestellt (Kante CP)

In Summe hängt es immer von den Anforderungen und der Kritikalität ab, welche Eigenschaften aus dem CAP-Theorem erfüllt werden sollen. Unterschiedliche Teile der Anwendung können außerdem unterschiedliche Eigenschaften erfüllen: Es muss nicht die Kante AP für das Gesamtsystem sein.

Wir haben in dieser Artikelserie bereits viele Themen diskutiert, die bei der Entscheidung Microservices oder Monolith eine Rolle spielen. Ein letztes und wichtiges Thema ist die Datenhaltung, über das natürlich auch Moni und Michi in ihrer Diskussion stolpern.

Moni: „Mir ist wichtig, dass ich dem Kunden zu jeder Zeit die richtige Auskunft geben kann. Wenn ein Menü nicht bestellbar ist, soll der Kunde das wissen. In einem Monolith kann ich das durch ACID-Transaktionen sicherstellen.“

Michi: „Wir werden viele Kunden haben. Unser System muss schnell reagieren und deshalb verfügbar sein. Das erreiche ich eher mit Eventual Consistency und glaube mir, letztendlich sind auch hier die Daten konsistent.“

Monolith garantiert ACID

Moni ist wichtig, dass die Anwendung zu jeder Zeit einen konsistenten Datenstand besitzt. In klassischen, monolithischen Architekturen wird Konsistenz durch ACID-Transaktionen sichergestellt. Das heißt: Eine Transaktion läuft vollständig erfolgreich durch oder alle Änderungen werden zurückgerollt. ACID ist ein Akronym und steht für die vier Eigenschaften von Transkationen Atomicity, Consistency, Isolation, Durability:

Atomarität (Abgeschlossenheit): „Alles oder nichts“. Alle Datenbank-Operationen, die im Rahmen einer Transaktion durchgeführt werden sollen, werden vollständig erfolgreich abgeschlossen oder – falls ein Fehler auftritt – werden alle getätigten Änderungen dieser Transaktion zurückgerollt (Rollback).

Konsistenzerhaltung: Eine Transaktion hinterlässt nach Abschluss einen konsistenten Datenbankzustand (unter anderem werden auch die definierten Integritätsbedingungen wie nicht NULL geprüft).

Isolation: Durch diese Eigenschaft wird verhindert, dass sich parallel laufende Transaktionen gegenseitig beeinflussen. Dies wird vor allem mithilfe von Sperren (Locks) realisiert.

Dauerhaftigkeit: Daten sind nach erfolgreichem Abschluss einer Transaktion dauerhaft in der Datenbank gespeichert. Ein Transaktionslog stellt dies sicher.

ACID in verteilten Systemen

ACID-Transaktionen sind vor allem für relationale Datenbanken gedacht. Die Erfüllung aller vier Eigenschaften in verteilten Systemen gestaltet sich als sehr aufwendig. Verteilte Transaktionen über Prozessgrenzen hinweg können aber prinzipiell mit dem XA-Protokoll technisch abgebildet werden. Dabei koordiniert ein zentraler Transaction Manager die an der Transaktion beteiligten Ressourcen. In der ersten Phase der Transaktion fragt der Transaction Manager bei den beteiligten Ressourcen, ob ein Commit möglich ist. Erst wenn alle Ressourcen bestätigt haben, dass der Commit möglich ist, wird der Commit auch ausgeführt. Wenn ein Fehler auftritt, koordiniert der Transaction Manager den Rollback. Da XA-Transaktionen in zwei Phasen durchgeführt werden, bezeichnet man diese auch als 2-Phase-Commits. XA-Transaktionen sind aufwendig zu implementieren und Datensätze werden sehr lange durch ein Lock gesperrt. Deshalb sollten XA-Transaktionen, so gut es geht, vermieden werden.

Single-source-of-truth in verteilten Systemen

Welche Möglichkeiten gibt es innerhalb eines verteilten Systems noch, damit Monis Wunsch erfüllt wird und die Anwendung immer die richtige Antwort liefert?

Eine offensichtliche Alternative ist eine geteilte Datenbank für alle Services innerhalb einer Microservice-Architektur. Das heißt: Alle Microservices greifen lesend und schreibend auf die gleichen Tabellen zu – der Datenaustausch zwischen Services entfällt. ABER: Dadurch wird nicht nur das Geheimnisprinzip gebrochen (Microservices können auf alles zugreifen, auch wenn sie das vielleicht gar nicht sollen), sondern auch die Datenhoheit. Es ist unklar, welcher Service für welche Daten verantwortlich ist. Außerdem betreffen Änderungen am Datenmodell dann immer alle Services: Die Microservices sind somit nicht mehr unabhängig voneinander. Eine gemeinsame Datenbank verhindert also einige der propagierten Vorteile einer Microservice-Architektur.

Eine weitere denkbare Alternative ist ein Shared Data Service. Also ein Service, der die gemeinsam benutzten Daten kapselt und über Schnittstellen (z. B. REST) anbietet. Zugriffsregeln auf Daten können über Rollen und Rechte im Shared Data Service sichergestellt werden, sodass Services nur auf das zugreifen können, worauf sie berechtigt sind. Änderungen am Datenmodell betreffen erst mal nur den Shared Data Service – allerdings sind alle weiteren Services an diesen einen Service gekoppelt. Hohe Kopplung gilt es in einer Microservice-Architektur zu vermeiden, denn sobald dieser eine Service nicht mehr verfügbar ist, ist die gesamte Anwendung nicht mehr verfügbar.

Das CAP-Theorem: die zusätzliche Komplexität in verteilten Systemen

Dass Verfügbarkeit und Konsistenz bei dieser Diskussion gegeneinander ankämpfen, ist kein Zufall: In verteilten Systemen gibt es neben den bereits bekannten Herausforderungen das CAP-Theorem als weitere Komplexität.

Das CAP-Theorem besagt, dass verteilte Systeme maximal zwei der drei CAP Eigenschaften gleichzeitig erfüllen können:

  • Consistency: Konsistenz über alle Knoten hinweg. Alle Knoten haben den gleichen Datenbestand. Nicht zu verwechseln mit dem „C“ aus ACID – diese betrifft die innere Konsistenz eines Datenbestands (z. B. auch ob Constraints verletzt wurden).
  • Availability: Verfügbarkeit als Ausdruck dafür, dass alle Anfragen an das System beantwortet werden.
  • Partition Tolerance: Ausfall einzelner Netzknoten oder Partition des Netzwerks wird toleriert und beeinflusst die Anwendung nicht.

In verteilten Systemen wird die Partitionstoleranz immer erfüllt (https://codahale.com/you-cant-sacrifice-partition-tolerance/). Also muss entschieden werden, ob ein System hochverfügbar (und dafür nicht konsistent) oder lieber konsistent (und dafür nicht immer verfügbar) sein soll.

Wenn man sich für die Kante CP entscheidet, werden in der Zeit, in der der Datenstand auf allen Knoten aktualisiert wird, keine Anfragen beantwortet. Erst nach Wiederherstellung der Konsistenz auf allen Knoten werden Anfragen beantwortet.

Das Base-Prinzip

Wenn man sich für die Kante AP entscheidet, kann es sein, dass das System für eine kurze Zeitspanne eine falsche Antwort als Rückgabe liefert. Dieses Prinzip wird auch BASE-Prinzip (als Gegensatz zu ACID: Säure vs. Base) genannt. Das Akronym BASE bedeutet „Basically Available, Soft State, Eventual Consistency). Eventual Consistency bedeutet nicht, dass Konsistenz eventuell eintritt, sondern dass die Konsistenz schlussendlich nach einer gewissen kurzen Zeit der Inkonsistenz hergestellt wird.

Auch Michi möchte für die Restaurant-App auf Eventual Consistency setzen und lieber hochverfügbar für die Anwender sein. Michi ist hier kein Einzelfall – viele verteilte Systeme setzen in der Regel auf die Kante AP. Damit die Verfügbarkeit erfüllt werden kann, benötigt der entsprechende Microservice alle Daten, die er für seine Aufgabe braucht, bei sich in der Datenbank. Die Replikation der Daten kann entweder push-basiert (z. B. über Messaging) oder pull-basiert (z. B. über REST) erfolgen.

Moni: „Wir werden viele Kunden haben. Unser System muss schnell reagieren und deshalb verfügbar sein. Das erreiche ich eher mit Eventual Consistency und glaube mir, letztendlich sind auch hier die Daten konsistent.“