Autonom und Asynchron

Will man große Softwaresysteme agil entwickeln, ist eine Zerlegung in autonom entwickelbare Microservices sinnvoll. Die Architekturprinzipien, um ein System in Microservices zu zerlegen, sind ähnlich den Prinzipien auf der Entwurfsebene von Klassen und Modulen. Großes Umdenken erfordert aber der Paradigmenwechsel in der Kommunikation zwischen den Entwurfseinheiten. Dieser Artikel beleuchtet die Muster und Lösungsansätze dieses Paradigmenwechsels.

“The biggest issue in changing a monolith into microservices lies in changing the communication pattern” [Fowler/Lewis 2014]

Paradigmenwechsel

Damit ein System in einem agilen Entwicklungsumfeld schnell und flexibel erweiterbar ist, müssen auf der Mikroarchitektur-Ebene Prinzipien eingehalten werden, die eine lose Kopplung von Entwurfseinheiten (Klassen, Modulen) bewirken. Als wichtigste Entwurfsprinzipien seien hier die SOLID-Prinzipien genannt [Martin2017].

Um mit mehreren Teams schlagkräftig und schnell agil entwickeln zu können, ist es sinnvoll, den Teams größtmögliche Autonomie zu ermöglichen. Das kann erreicht werden, indem das System in unabhängig entwickel- und deploybare Microservices zerlegt wird. Hier greifen für die Makroarchitektur ähnliche Prinzipien wie für die Mikroarchitektur. Die einzelnen Services werden, dem Single-Responsibility-Prinzip entsprechend, in unabhängige Systeme mit eigener Datenhaltung und eigenen UI-Bestandteilen zerlegt [SelfContainedSystems2018]. Die Systemgrenzen verlaufen entlang von Bounded Contexts, wie sie in Domain Driven Design beschrieben sind [Evans2003].

Betrachtet man aber für die Makroarchitektur die Kommunikation der Systembestandteile, so gibt es einen großen Paradigmenwechsel:

Statt synchroner Methoden- oder Funktionsaufrufe innerhalb eines Moduls empfiehlt sich für die verteilten Systembestandteile im Normalfall eine asynchrone Kommunikationsform.

Warum asynchrone Kommunikation so wichtig ist, wird in der folgenden Abbildung verdeutlicht: 

In einer verteilten Microservices-Architektur können potentiell viele Services miteinander kommunizieren. Ein Request, der von einem externen Client, wie einem Webbrowser, kommt, kann zu einer Kaskade von Aufrufen führen. Im Beispiel fragt eine Kunde einen Web-Endpunkt für einen Warenkorb an. Der Warenkorb benötigt für die Darstellung der enthaltenen Produkte die Produktdaten, die er vom Produkt-Service erhält. Der Produkt-Service wiederum benötigt Informationen zur Verfügbarkeit vom Warenbestands-Service. Wird jeweils per synchronem Aufruf auf die Antwort des nachfolgenden Service gewartet, so hängt die Antwortzeit von der Geschwindigkeit und Stabilität der gesamten Aufrufkette ab. Ist das Netzwerk beispielsweise zwischen Produkt- und Warenbestands-Service gestört oder der Produkt-Service ist unter großer Last, dann verzögert sich die Antwort an den Kunden. Werden von den Services fehlgeschlagene Requests in kurzer Zeit automatisch wiederholt, so verschärft sich gegebenenfalls die Situation durch immer größere Mengen geblockter Ressourcen (Connections, Threads).

Welche Gefahren für die Gesamtstabilität bei synchroner Kommunikation lauern, wird in diesem kleinen Beispiel bereits deutlich. Bei großen Systemen mit hunderten von Services wird diese Gefahr noch viel größer [Vogels2016].

Das Leitbild für die Kommunikation in einem verteilten Microservices-System ist deshalb asynchrone Kommunikation. Jeder Service soll möglichst autonom schnell Antworten liefern können, ohne auf die Antwort anderer Services warten zu müssen.

Asynchron

Im Folgenden wird dargestellt, wie der Paradigmenwechsel von synchroner auf asynchrone Kommunikation umgesetzt werden kann. Im Wesentlichen können zwei Kommunikationsformen unterschieden werden:

  • Queries: Ein Service benötigt Informationen von einem anderen Service.
  • Commands: Ein Service schickt Befehle an einen anderen Service.

Queries

In Abbildung 1 ist dargestellt, dass der Warenkorb-Service Produktdaten vom Produkt-Service benötigt, um den Warenkorb mit den darin enthaltenen Produkten darstellen zu können.

Um die synchrone Kommunikation zu vermeiden, muss der Warenkorb-Service die Produktdaten zeitlich entkoppelt erhalten können. Der Warenkorb-Service muss den für seinen Betrieb notwendigen Ausschnitt an Produktdaten für sich replizieren und wird dadurch unabhängig von der Verfügbarkeit des Produkt-Services.

Die entkoppelte Kommunikation kann über Events realisiert werden:

Gibt es neue Produkte oder Änderungen an bestehenden Produktdaten, so sendet der Produkt-Service Events, in denen die Änderungen der Produktdaten enthalten sind. Wie das Senden von Events technisch umgesetzt werden kann, werden wir demnächst in einem Folge-Artikel untersuchen.

Der Warenkorb-Service konsumiert diese Produktdaten-Ereignisse und aktualisiert damit seinen replizierten Produktdaten-Ausschnitt.

Commands

In der folgenden Abbildung ist als Beispiel dargestellt, dass auf einer Warenkorb-Seite über einen Bestell-Button eine Bestellung eingeleitet werden soll.

Im Ein-Prozess-Raum eines Monolithen würde der Warenkorb-Service durch einen Methoden-Aufruf am Bestell-Service-Objekt synchron eine Operation aufrufen und unmittelbar eine Rückmeldung bekommen, ob die Bestellung eingeleitet werden konnte.

Im Umfeld verteilter Microservices ist die Situation komplexer. Der Warenkorb-Service muss den Bestell-Service über eine neu anzulegende Bestellung informieren. Der Warenkorb-Service muss aber darauf vorbereitet sein, dass nicht unmittelbar eine Antwort vom Bestell-Service kommt. Das Absenden des Requests und das Auswerten der Antwort müssen zeitlich entkoppelt werden.

Die entkoppelte Kommunikation kann über Events realisiert werden: Der Warenkorb-Service sendet ein Event („Bestellung anlegen“) mit den notwendigen Informationen. Der Warenkorb-Service merkt sich für den aktuellen Warenkorb den Zustand „Warte auf Bestellungs-Bestätigung“. Erhält der Warenkorb-Service später eine Rückmeldung vom Bestell-Service („Bestellung erfolgt“ oder „Bestellung fehlgeschlagen“), dann wird der Zustand des Warenkorbs im Warenkorb-Service aktualisiert.

Kommt das Antwort-Event des Bestell-Services schnell genug, dann kann dem Kunden im Browser unmittelbar das Ergebnis präsentiert werden. Für den Kunden wirkt es, als ob eine synchrone Kommunikation vorlag. Kommt die Antwort nicht schnell genug, so kann dem Kunden eine Verzögerung signalisiert werden. Im Browser kann gegebenenfalls automatisch in Intervallen der aktualisierte Zustand im Warenkorb-Service abgefragt werden.

Signalisiert der Bestellservice im Antwort-Event eine Fehlersituation, so muss der Warenkorb-Service den Zustand des Warenkorbs entsprechend zurücksetzen.

Das hier beschriebene Vorgehen ist ein einfaches Beispiel für die Umsetzung einer verteilten Transaktion mittels des Saga-Patterns [GarcaaSalem1987, Saga2018].

Herausforderungen asynchroner Kommunikation

Die Vorteile asynchroner Kommunikation wurden oben dargelegt. Es gibt aber zwei daraus resultierende Herausforderungen: Datenkonsistenz und Datenduplikation

Datenkonsistenz

In einem verteilten System ist es gemäß des CAP-Theorems [CAP2018] nicht möglich, gleichzeitig immer verfügbar, datenkonsistent und tolerant bezüglich Netzausfällen zu sein.

Man muss sich entscheiden, ob man jederzeit ein konsistentes Gesamtsystem haben möchte und dafür die permanente Hochverfügbarkeit im Fehlerfall opfert.

Oder man entscheidet sich, hochverfügbar und tolerant gegenüber dem Teilausfall von Netzen zu sein und nimmt dafür temporäre Dateninkonsistenzen in Kauf. Das daraus resultierende Datenkonsistenzmodell wird als "Eventual Consistency" bezeichnet und lässt sich mit den oben beschriebenen Eventing-Mechanismen umsetzen. Temporäre Inkonsistenzen treten so lange auf, bis alle Messages zu dem Ereignis verarbeitet werden konnten. Wenn beispielsweise der Produkt-Service Datenänderungen propagiert, der Warenkorb-Service aber das Event noch nicht verarbeitet hat, so weichen die Darstellung im Warenkorb und die Darstellung auf der Produktseite kurzfristig voneinander ab.

Für viele Microservice-Systeme ist Eventual Consistency ausreichend und die damit erreichbare Hochverfügbarkeit der Services wichtiger.

Datenduplikation

In der Softwareentwicklung versuchen wir grundsätzlich, Duplikationen zu vermeiden. Duplizierte Daten sorgen für höheren Aufwand, da bei jeder Änderung für einen konsistenten Gesamtstand alle Duplikate aktualisiert werden müssen.

In einem verteilten System mit einem Eventual-Consistency-Modell lassen wir aber Duplikationen explizit zu.

Die Duplikation von Daten bietet dann sogar besondere Chancen: Jeder Service kann für sich selbst bestimmen, wie duplizierte Daten optimal strukturiert, persistiert und indiziert werden. In unserem Beispiel können die umfangreichen Produktdaten des Produkt-Services vom Warenkorb-Service in einer für den Warenkorb reduzierten und optimierten Weise gespeichert werden.

Zusammenfassung

Autonom entwickelbare Microservices helfen in einem skalierten agilen Projektumfeld, beweglich zu sein, schnell zu entwickeln und auszuliefern.

Neben der strukturellen Zerlegung eines Systems in Teilsysteme besteht die größte Herausforderung eines verteilten Systems darin, ein asynchrones Kommunikationsmodell zu implementieren, um widerstandsfähig gegenüber Netzlatenzen und Serverausfällen zu sein. Commands und Queries können über Events realisiert werden. Häufig folgt daraus ein Datenmodell, das „eventual consistent“ ist, aber durch replizierte Daten hochperformante, entkoppelte, autonome Services ermöglicht.

 

Referenzen

[FowlerLewis2014] Martin Fowler, James Lewis: Microservices a definition of this new architectural term, martinfowler.com/articles/microservices.html

[Evans2003] Eric Evans: Domain-Driven Design, Addison-Wesley 2003 [Martin2017] Robert Martin: Clean Architecture, Prentice Hall 2017

[Vogels2016] Werner Vogels, twitter.com/Werner/status/741673514567143424

[GarcaaSalem1987] Hector Garcaa-Molrna, Kenneth Salem: Sagas, www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf

[Saga2018] microservices.io/patterns/data/saga.html

[SelfContainedSystems2018] scs-architecture.org

[CAP2018] de.wikipedia.org/wiki/CAP-Theorem