Beliebte Suchanfragen

Cloud Native

DevOps

IT-Security

Agile Methoden

Java

//

Kaffeeküchengespräche – Microservices und Domain Driven Design

7.6.2015 | 9 Minuten Lesezeit

Neulich in der Kaffeeküche im Gespräch mit einem Kollegen, der gerade von einem zweijährigen Sabbatical im Dschungel Brasiliens zurückgekehrt ist.

Wenn man sich so die aktuellen Programme der Konferenzen ansieht, dann scheint dieses Microservices-Ding ja ganz groß zu sein. Worum geht’s dabei eigentlich?

Nichts davon gehört bisher? Microservices ist ein Architekturstil, der in den letzten zwei Jahren immer mehr an Fahrt aufgenommen hat. Im Prinzip geht’s darum, kleine, autonome Einheiten zu entwickeln, die sich darauf konzentrieren, eine Sache gut zu implementieren. So ein Microservice besitzt eine API, die nicht an eine bestimmte Technologie gekoppelt ist, also üblicherweise über HTTP oder Messaging läuft. Die Autonomie hat dabei drei Aspekte:

  • Ein eigener Prozess pro Microservice.
  • Freie Technologiewahl innerhalb des Microservices für den vollen Stack inklusive Persistenz.
  • Jederzeit mögliches unabhängiges Deployment.

Puh, also extrem verteilte Systeme. Das hört sich nicht nach Spaß an. Was verspricht man sich denn davon?

Ja, verteilte Systeme sind nicht trivial. Also, zum einen verspricht man sich Vorteile von der erwähnten Technologiefreiheit: Dadurch, dass jeder Microservice in einem eigenen Prozess läuft, die API technologieunabhängig ist und jeder Microservice den vollen Stack inklusive Persistenz umfasst, kann innerhalb eines Microservices frei gewählt werden, mit welcher Technologie er implementiert wird – Choose the right tool for the job. Das birgt natürlich auch die Gefahr, dass man schnell einen bunten Zoo von Technologien im Haus hat. Wichtiger ist vielleicht der folgende Aspekt: selbst wenn man sich für einen oder mehrere Standard-Stacks entscheidet, so kann man diese jederzeit ändern, ohne bereits bestehende Microservices anpassen zu müssen. Bei monolithischen Systemen hängt man häufig auf veralteten Stacks und Bibliotheksversionen fest, weil eine Migration der bestehenden Funktionalität teuer ist und keinen Businessvalue hat. Hier kommen wir direkt zu einem weiteren Vorteil von Microservices – da ein Microservice klein ist, kann man ihn ohne Probleme in einer endlichen Zeit komplett neu schreiben. Designed for Replaceability.

Das hört sich reizvoll an. Ich habe bisher keinen Kunden kennengelernt, der nicht irgendwo Technologien modernisieren wollte, das aber aus den genannten Gründen nicht getan hat.

Ja, das ist ein wichtiger Punkt. Der zweite, vielleicht sogar noch wichtigere Punkt betrifft das jederzeit mögliche unabhängige Produktionsdeployment. Das geht, weil wir keinen komplizierten, langwierigen Buildprozess haben, sondern kleine Einheiten, und es geht, weil wir den kompletten Stack in der Hand haben. Hier wären wir also beim Thema Continuous Delivery. Das bringt uns in erster Linie Reaktionsgeschwindigkeit. Warum Reaktionsgeschwindigkeit immer wichtiger wird, hat Uwe in einem sehr lesenswerten Artikel dargelegt: The need for speed – A story about DevOps, microservices, continuous delivery and cloud computing . Man will durch eine Microservice-Architektur nicht die Kosteneffizienz steigern, sondern die Geschwindigkeit, mit der man Änderungen, Updates, neue Produkte an den Markt bringen kann.

Ach, es geht mal nicht um die Kosten?

Nein, erst einmal bedeutet eine Microservice-Architektur Investitionen. Man muss schon echte Schmerzen mit der fehlenden Reaktionsgeschwindigkeit haben. Aber wenn ich die Wartungshölle von großen Monolithen betrachte, würde ich mal davon ausgehen, dass sich das auf mittelfristige Sicht auch positiv auf die Kosten auswirkt.

Okay, weitere Vorteile von Microservices?

Zwei technische Punkte habe ich gerade noch – Resilience und Scaling. In einem Monolithen kann ein außer Kontrolle geratener Thread die ganze Anwendung herunterreißen. In einer Microservice-Architektur stirbt erst einmal nur ein Microservice, der Rest läuft weiter. Das Thema Resilience birgt aber auch Herausforderungen in einer Microservice-Architektur, da in verteilten Systemen naturgemäß mehr schiefgehen kann. Falsch eingestellte Timeouts und nicht antwortende Anwendungen können auch leicht das ganze System zu Fall bringen. Zum Thema Resilience gibt es aber eine Reihe guter Patterns und mit Hystrix eine viel verwendete Open-Source-Implementierung, um das Thema in den Griff zu bekommen.
Der wichtigste Punkt beim Scaling ist, dass man den Monolithen nur komplett skalieren kann. Wenn man aber nur bestimmte Teile der Anwendung skalieren will, weil nur diese die Skalierung brauchen, so geht das nicht. Bei eigenständigen Microservices kann man natürlich einzelne Services beliebig skalieren.
Es gibt bestimmt noch mehr Punkte, aber mehr fallen mir gerade nicht ein.

Hört sich erst einmal ganz gut an, so wie du das hier erzählst, aber ob das so durchsetzbar ist. Du sagst mehrfach, dass der Microservice den vollen Stack umfassen soll, aber die Datenbank ist nun mal häufig in zentraler Hand…

Es geht nicht ohne eine Änderung der Organisationsstruktur. Ist ja klar: wenn man sechs Wochen im voraus Datenbankänderungen beantragen muss, ist der Geschwindigkeitsvorteil natürlich komplett dahin. Wenn man vier zentrale Produktionsdeployments im Jahr hat und davon nicht abweichen will, hat man auch keinen Geschwindigkeitsvorteil mehr.

Okay, noch eine Frage, die mir durch den Kopf geht: wie klein ist eigentlich klein? Mir ist das ganze bisher zu schwammig.

Ja, die Größe und der Schnitt von Microservices, ein wichtiges und nicht ganz einfaches Thema. Die Argumentationen, die ich bisher am schlüssigsten fand, bedienten sich bei Begriffen aus Domain Driven Design, dem Buch von Eric Evans.

Äh ja, das habe ich auch zu Hause im Regal stehen. Sollte ich vielleicht mal wieder reinschauen. Eins der Bücher, das ich jedem Junior-Dev empfehle, selbst aber vor sieben Jahren das letzte Mal gelesen habe.

Das geht wahrscheinlich vielen so. Die wichtigste Begriff ist vermutlich Bounded Context. Stell dir vor, du hast ein sehr großes Modell mit vielen Entitäten und ein potenziell großes Team, das dieses Modell in Software gießen soll, dann ist das Prinzip der Bounded Contexts ein Mittel, mit dieser Komplexität klarzukommen. Modelle haben immer einen Kontext, jetzt geht es darum, diesen explizit zu machen und zu klären, wie die Verbindung zwischen den Kontexten aussieht. In dem Beispiel in Fowler’s Bliki haben wir zwei Kontexte, einmal Sales und einmal Support.

Sie beinhalten unterschiedliche Objekte, aber zwei von denen kommen in beiden Kontexten vor: Customer und Product. Wichtig ist zu verstehen, dass Customer und Product in den verschiedenen Kontexten unterschiedliche Dinge sind. Im Kontext Support hängt am Customer eine Liste von Tickets, während im Kontext Sales ein Territory dem Customer zugeordnet ist. Wir reden hier immer noch um Modellierung von Domänen, nicht um die technische Umsetzung, aber wenn wir mal den Umsetzungsaspekt betrachten, so kann man Systeme anhand von Kontextgrenzen schneiden. Ein Sales-System speichert die Customer-Attribute, die Sales-spezifisch sind, und ein Support-System speichert die Attribute, die Support-spezifisch sind. Um die Verbindung zwischen den beiden Systemen herzustellen, verwendet man in der Regel eine eindeutige ID zur Identifizierung des Customers.

Okay, okay, habe ich verstanden, aber was hat das mit Microservices zu tun?

So ein Bounded Context ist eine natürliche Obergrenze für einen Microservice. Wir wollen ja, dass er klein und fokussiert ist. Ein Microservice, der für mehrere Bounded Contexts verantwortlich ist, ist mit Sicherheit nicht fokussiert und sehr wahrscheinlich nicht klein. Dazu kommt, dass Kontexte ja nicht willkürlich gezogen werden, sondern implizit da sind und sich häufig auch in der Organisationsstruktur wiederfinden. Systeme, die mehrere Bounded Contexts verantworten, werden demnach häufig auch von verschiedenen Teams und Fachabteilungen weiterentwickelt. Eine weitere Obergrenze für einen Microservice ist die Teamgröße. Es ist wichtig, dass der Service von einem kleinen Team komplett entwickelt und betreut werden kann, inklusive Betreuung des vollen Stacks, Deployment nach Produktion und zumindest teilweise Betrieb.

Okay, gibt es auch eine Untergrenze für die Größe von Microservices?

Ja, hier kommt ein weiterer Begriff aus Domain Driven Design hinzu: Aggregate . Ein Aggregate ist quasi der Zusammenschluss von Entitäten, die in einer Transaktion gespeichert werden müssen. Es sollte keine Transaktionen geben, die Aggregatsgrenzen überschreiten. Wenn man Microservices schreibt, die nur Teile eines Aggregates schreiben, kommt man zwangsläufig zu dem Problem, entweder verteilte Transaktionen über Microservices hinweg implementieren zu wollen (was totaler Quatsch ist), oder komplizierte Kompensationslogik implementieren zu müssen. Zwei Anmerkungen dazu noch:

  • Hier geht es um sofortige Konsistenz nach Ausführen des Speicherns. Eventual Consistency, also die Sicherheit, dass irgendwann Konsistenz hergestellt ist, kann auch gut über Microservices-Grenzen hinweg sichergestellt werden. Das geschieht dann üblicherweise über Events. System A im Bounded Context Warenkorb speichert zum Beispiel die Bestellung des Kunden und schickt dann ein entsprechendes Event raus, und System B im Bounded Context Versand horcht auf dieses Event und bereitet den Versand der Ware vor.
  • Außerdem geht es bei der Aggregate-Betrachtung um das Schreiben von Daten, hier ist die Transaktion wichtig. Schreiben und Lesen können natürlich getrennt werden, das wird gerade in letzter Zeit mit CQRS ein immer größeres Thema. Man kann so Microservices schreiben, die auf das Lesen bestimmter zusammengestellter Daten spezialisiert sind. Und diese Zusammenstellung kann beliebig nach Anforderung geschehen. Wenn der Microservice beispielsweise Teile von Aggregate A und Teile von Aggregate B zusammen liefern soll, dann kann er auf die Events der beiden schreibenden Microservices für Aggregate A und Aggregate B horchen und genau die Teile, die er braucht, in seiner Persistenz duplizieren. Dieser Microservice bearbeitet also keine vollständigen Aggregates, aber das ist kein Problem, da es nur ums Lesen geht. Wichtig ist, dass es immer genau einen Verantwortlichen für das Schreiben der Daten gibt.

Ein Aggregate ist der Zusammenschluss von Entitäten, die in einer Transaktion gespeichert werden müssen. Wenn das so definiert ist, hat man da nicht schnell sehr große Aggregates? Es wird doch immer irgendwelche Anwendungsfälle geben, in denen zwei Dinge in einer Transaktion gespeichert werden müssen.

Hm, da würde ich erst einmal evaluieren, ob nicht Eventual Consistency doch ausreicht. Die Anforderung, dass Dinge sofort konsistent gespeichert sein sollen, kommt ja häufig von der GUI – ich löse irgendetwas aus und will das Ergebnis sofort sehen, auch wenn es zwei verschiedene Aggregates betrifft. Ich drücke auf „Kaufen“ und will das Produkt nach dem Aktualisieren der Seite unter „Meine Bestellungen“ sehen. Sagen wir mal,„Warenkorb“ und „Bestellungen“ seien zwei verschiedene Systeme. Aus Businesssicht wäre Eventual Consistency zwischen den Systemen okay – solange der Inhalt des Warenkorbs irgendwann zu einer Bestellung wird, bekomme ich mein Geld. Wir reden hier bei Eventual Consistency ja auch nicht von Tagen, sondern von Millisekunden. Aus GUI-Sicht wollen wir den Kunden nicht verwirren und seine Bestellungen direkt anzeigen. Um das zu gewährleisten, gibt es verschiedene Möglichkeiten – Polling der GUI auf Bestellungen, Eventing über Web-Sockets etc. Naja, du siehst schon, es kommt sehr auf den Anwendungsfall und die fachlichen Anforderungen an.

Könnte nicht einfach der Warenkorb-Microservice den Bestellungen-Microservice synchron aufrufen? Wenn dann der Warenkorb-Microservice an die GUI zurückmeldet, dass er fertig ist, sind doch auch die Bestellungen gespeichert.

Ja, aber das hat diverse Nachteile. Bei einer Kommunikation über Events schickt der Warenkorb-Service immer dann ein Event, wenn „Kaufen“ gedrückt wurde. Dieses Event beinhaltet alle wichtigen Daten. Jeder, der möchte, kann dieses Event abonnieren. Der Warenkorb-Service weiß gar nicht, welche Systeme das sind, es ist ihm völlig egal. In unserem Fall ist es der Bestellungen-Service, aber man kann sich gut vorstellen, dass noch andere Services an diesen Informationen interessiert sind. Vielleicht irgendwelche Services, die Statistiken über Käufe erstellen. Oder der Service, der Recommendations für den Benutzer erzeugt. Wenn der Warenkorb-Service aktiv aufruft, dann muss er den Empfänger kennen, muss dessen API verstehen etc. Es ist eine deutlich engere Kopplung und sorgt auch für mehr Kommunikationsbedarf zwischen Teams. Und dazu kommt noch der technische Aspekt. Ein Event kann man gut transaktionssicher verschicken und sich sicher sein, dass es nicht verloren geht. Ein synchroner HTTP-Aufruf kann leichter aus technischen Gründen schiefgehen, und was macht man dann? Kompensation? Zurückrollen der Warenkorb-Transaktion?

Verstehe ich ja. Trotzdem fühlt sich dieser Messaging-Kram noch ein bisschen nach Overhead an. Mir schwirrt etwas der Kopf.

Dann lass es mal sacken. Wichtig ist wie in allen Fragen in der IT, dass der gesunde Menschenverstand immer noch die wichtigste Ressource ist. Wir können ja gerne demnächst mal weiter diskutieren.

Beitrag teilen

Gefällt mir

1

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.