Kapitel 4. Rightsizing deiner Microservices: Service-Grenzen finden
Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com
Einer der schwierigsten Aspekte beim Aufbau eines erfolgreichen Microservice-Systems ist die Festlegung der richtigen Microservice-Grenzen. Es leuchtet intuitiv ein, dass die Aufteilung einer großen Codebasis in kleinere, einfachere und lose gekoppelte Teile die Wartbarkeit verbessert, aber wie entscheiden wir, wo und wie wir den Code aufteilen, um diese gewünschten Eigenschaften zu erreichen? Welche Regeln verwenden wir, um zu wissen, wo ein Dienst endet und ein anderer beginnt? Die Beantwortung dieser grundlegenden Fragen ist eine Herausforderung. Viele Teams, für die Microservices neu sind, stolpern über sie. Eine falsche Abgrenzung der Microservices kann die Vorteile von Microservices erheblich schmälern oder in manchen Fällen sogar das gesamte Projekt zum Scheitern bringen. Es ist daher nicht verwunderlich, dass die häufigste und drängendste Frage, die Microservices-Praktiker stellen, lautet: Wie kann eine größere Anwendung richtig in eine Sammlung von Microservices aufgeteilt werden?
In diesem Kapitel befassen wir uns eingehend mit der führenden Methodik für die effektive Analyse, Modellierung und Zerlegung großer Domänen (Domain-Driven Design), erläutern die Effizienzvorteile des Event Storming für die Domänenanalyse und stellen abschließend die Universal Sizing Formula vor, eine einzigartige Anleitung für die effektive Dimensionierung von Microservices.
Warum Grenzen wichtig sind, wann sie wichtig sind und wie man sie findet
Schon im Titel des Architekturmusters steht das Wort Mikro - dieArchitektur, die wir entwerfen, ist die der "Mikro"-Dienste! Aber wie "mikro" sollten unsere Dienste sein? Wir messen natürlich nicht die physische Länge von etwas und nehmen an, dass "Mikro" ein Millionstel eines Meters bedeutet (d. h. der Basiseinheit der Länge im Internationalen Einheitensystem). Was bedeutet also Mikro für unsere Zwecke? Wie sollen wir unser größeres Problem in kleinere Dienste aufteilen, um die versprochenen Vorteile von "Mikro"-Diensten zu erreichen? Vielleicht könnten wir unseren Quellcode auf Papier ausdrucken, alles zusammenkleben und die buchstäbliche Länge davon messen? Oder, Spaß beiseite, sollten wir uns an der Anzahl der Zeilen in unserem Quellcode orientieren und diese Zahl klein halten, um sicherzustellen, dass jeder unserer Microservices auch klein genug ist? Was aber ist "ausreichend"? Vielleicht legen wir einfach willkürlich fest, dass jeder Microservice nicht mehr als 500 Codezeilen haben darf? Wir könnten auch die Grenzen an den bekannten funktionalen Kanten unseres Quellcodes ziehen und sagen, dass jede granulare Fähigkeit, die durch eine Funktion im Quellcode unseres Systems repräsentiert wird, ein Microservice ist. Auf diese Weise könnten wir unsere gesamte Anwendung z. B. mit serverlosen Funktionen aufbauen und jede dieser Funktionen als Microservice deklarieren. Sauber und einfach! Stimmt's? Vielleicht nicht.
In der Praxis wurde jeder dieser vereinfachenden Ansätze ausprobiert und sie haben alle erhebliche Nachteile. Während Codezeilen (SLOC) in der Vergangenheit gerne als Maß für den Aufwand bzw. die Komplexität verwendet wurden, ist inzwischen allgemein anerkannt, dass sie ein schlechtes Maß sind, um die Komplexität oder den wahren Umfang eines Codes zu bestimmen, und dass sie leicht manipuliert werden können. Selbst wenn unser Ziel darin bestünde, "kleine" Dienste zu erstellen, in der Hoffnung, sie einfach zu halten, wären Codezeilen daher ein schlechter Maßstab.
Das Ziehen von Grenzen an funktionalen Kanten ist noch verlockender. Und mit der zunehmenden Beliebtheit von serverlosen Funktionen wie den Lambda-Funktionen von Amazon Web Services ist die Versuchung noch größer geworden. Aufgrund der Produktivität und der weiten Verbreitung von AWS Lambdas haben sich viele Teams darauf gestürzt, diese Funktionen zu "Microservices" zu erklären. Wenn du diesen Weg einschlägst, gibt es eine Reihe von Problemen, von denen die wichtigsten sind:
- Das Ziehen von Grenzen auf der Grundlage von technischen Bedürfnissen ist ein Anti-Muster
-
Laut Lewis und Fowler sollten Microservices "um die geschäftlichen Fähigkeiten herum organisiert werden", nicht um technische Anforderungen. Ähnlich empfiehlt Parnas in einem Artikel aus dem Jahr 1972, Systeme auf der Grundlage einer modularen Kapselung von Designänderungen im Laufe der Zeit zu zerlegen. Keiner der beiden Ansätze ist zwangsläufig eng mit den Grenzen der serverlosen Funktionen verknüpft.
- Zu viel Granularität, zu früh
-
Eine explosive Granularität zu Beginn des Lebenszyklus eines Microservices-Projekts kann zu einer erdrückenden Komplexität führen, die das Microservices-Projekt zum Erliegen bringt, noch bevor es überhaupt eine Chance hat, erfolgreich zu sein.
In Kapitel 1 haben wir das Hauptziel einer Microservices-Architektur dargelegt: Es geht in erster Linie um die Minimierung der Koordinationskosten in einer komplexen Multiteam-Umgebung, um eine Harmonie zwischen Geschwindigkeit und Sicherheit zu erreichen, und zwar im großen Maßstab. Deshalb sollten die Dienste so gestaltet werden, dass der Koordinationsaufwand zwischen den Teams, die an verschiedenen Microservices arbeiten, möglichst gering ist. Wenn wir den Code jedoch auf eine Weise in Funktionen aufteilen, die nicht unbedingt zu einer minimalen Koordination führt, werden wir am Ende mit falsch dimensionierten Microservices dastehen. Die Annahme, dass die Aufteilung des Codes in serverlose Funktionen den Koordinationsaufwand verringert, ist ein Irrglaube.
Wir haben bereits darauf hingewiesen, dass ein wichtiger Grund für die Vermeidung eines größenbasierten oder funktionsorientierten Ansatzes bei der Aufteilung einer Anwendung in Microservices die Gefahr einer verfrühten Optimierung ist, d. h. zu viele und zu kleine Services zu Beginn der Microservices-Reise zu haben. Frühe Anwender von Microservices, wie Netflix, SoundCloud, Amazon und andere, hatten schließlicheine Menge Microservices! Das bedeutet jedoch nicht, dass diese Unternehmen vom ersten Tag an mit Hunderten von sehr granularen Microservices begonnen haben. Vielmehr haben sie sich nach jahrelanger Entwicklung für eine große Anzahl von Microservices entschieden, nachdem sie die betriebliche Reife erlangt hatten, um die mit der hohen Granularität von Microservices verbundene Komplexität zu bewältigen.
Vermeide es, zu früh zu viele Microservices zu erstellen
Die Dimensionierung von Diensten in einer Microservices-Architektur ist mit Sicherheit eine Reise, die sich mit der Zeit entfalten sollte. Ein sicherer Weg, das ganze Vorhaben zu sabotieren, ist der Versuch, ein zu granulares System zu entwerfen.
Egal, ob du an einem Greenfield-Projekt arbeitest oder einen bestehenden Monolithen zerlegst, der Ansatz sollte auf jeden Fall sein, mit nur einer Handvoll Services zu beginnen und die Anzahl der Microservices im Laufe der Zeit langsam zu erhöhen. Wenn das dazu führt, dass einige deiner Microservices anfangs größer sind als im Zielzustand, ist das völlig in Ordnung. Du kannst sie später aufteilen.
Selbst wenn wir nur mit ein paar Microservices beginnen und es langsam angehen lassen, brauchen wir eine zuverlässige Methode, um die Größe der Microservices zu bestimmen. Im Folgenden werden wir bewährte Methoden untersuchen, die in der Branche erfolgreich eingesetzt werden.
Domain-Driven Design und Microservice-Grenzen
Als es darum ging, die bewährten Methoden für das Design von Microservices herauszufinden, stellte Sam Newman in seinem Buch Building Microservices (O'Reilly) einige grundlegende Regeln auf. Er schlug vor, dass wir bei der Abgrenzung von Diensten ein solches Design anstreben sollten, dass die daraus resultierenden Dienste sind:
- Lose gekoppelt
-
Die Dienste sollten relativ unabhängig voneinander sein, damit eine Codeänderung in einem von ihnen keine Auswirkungen auf die anderen hat. Außerdem sollten wir die Anzahl der verschiedenen Arten von Laufzeitaufrufen von einem Dienst zum anderen begrenzen, denn abgesehen von dem potenziellen Leistungsproblem kann die geschwätzige Kommunikation auch zu einer engen Kopplung der Komponenten führen. Wenn wir unseren Ansatz der "Koordinationsminimierung" verfolgen, liegt der Vorteil der losen Kopplung der Dienste auf der Hand.
- Hochgradig kohäsiv
-
Die Funktionen eines Dienstes sollten eng miteinander verknüpft sein, während nicht verwandte Funktionen an anderer Stelle gekapselt sein sollten. Wenn du also eine logische Funktionseinheit ändern musst, solltest du sie an einer Stelle ändern können, um die Zeit bis zur Freigabe dieser Änderung zu minimieren (eine wichtige Kennzahl). Müssten wir dagegen den Code in einer Reihe von Diensten ändern, müssten wir viele verschiedene Dienste gleichzeitig freigeben, um die Änderung umzusetzen. Das würde einen hohen Koordinationsaufwand bedeuten, vor allem wenn diese Dienste mehreren Teams gehören. Und das würde unser Ziel, die Koordinationskosten zu minimieren, direkt gefährden.
- Abgestimmt auf die Geschäftsmöglichkeiten
-
Da die meisten Anfragen zur Änderung oder Erweiterung von Funktionen von geschäftlichen Anforderungen ausgehen, ist es nur logisch, dass die oben genannten ersten und zweiten Design-Anforderungen leichter zu erfüllen sind, wenn unsere Grenzen eng mit den Grenzen der geschäftlichen Möglichkeiten abgestimmt sind. In den Tagen der monolithischen Architekturen haben Softwareentwickler oft versucht, sich auf "kanonische Datenmodelle" zu einigen. Die Praxis hat jedoch immer wieder gezeigt, dass detaillierte Datenmodelle zur Modellierung der Realität nicht lange Bestand haben - sie ändern sich recht häufig und die Standardisierung führt zu häufigen Nacharbeiten. Was stattdessen dauerhafter ist, ist eine Reihe von Geschäftsfunktionen, die deine Subsysteme bereitstellen. Ein Buchhaltungsmodul wird immer in der Lage sein, die gewünschten Funktionen für dein Gesamtsystem bereitzustellen, unabhängig davon, wie sich sein Innenleben im Laufe der Zeit entwickelt.
Diese Gestaltungsprinzipien haben sich als sehr nützlich erwiesen und wurden von vielen Microservices-Praktikern übernommen. Allerdings handelt es sich dabei um recht hoch angesetzte Prinzipien, die wohl nicht die konkrete Anleitung für die Dimensionierung von Services bieten, die Praktiker im Alltag benötigen. Auf der Suche nach einer praktischeren Methode wandten sich viele dem Domain-Driven Design zu.
Die als Domain-Driven Design (DDD) bekannte Software-Entwurfsmethodik geht der Microservices-Architektur deutlich voraus. Sie wurde 2003 von Eric Evans in seinem gleichnamigen, bahnbrechenden Buch Domain-Driven Design vorgestellt : Tackling Complexity in the Heart of Software (Addison-Wesley). Die wichtigste Prämisse der Methode ist die Behauptung, dass wir bei der Analyse komplexer Systeme nicht nach einem einzigen, einheitlichen Domänenmodell suchen sollten, das das gesamte System repräsentiert. Stattdessen, so Evans in seinem Buch:
Bei großen Projekten existieren mehrere Modelle nebeneinander, und das funktioniert in vielen Fällen gut. Verschiedene Modelle sind in verschiedenen Kontexten anwendbar.
Nachdem Evans festgestellt hatte, dass ein komplexes System im Grunde eine Sammlung von mehreren Domänenmodellen ist, machte er den entscheidenden zusätzlichen Schritt, indem er den Begriff des begrenzten Kontexts einführte. Konkret erklärte er Folgendes:
Ein Bounded Context definiert den Anwendungsbereich eines jeden Modells.
Begrenzte Kontexte ermöglichen die Implementierung und die Ausführung verschiedener Teile des größeren Systems zur Laufzeit, ohne die unabhängigen Domänenmodelle in diesem System zu beeinträchtigen. Nachdem er begrenzte Kontexte definiert hatte, lieferte Eric auch eine hilfreiche Formel zur Ermittlung der optimalen Kanten eines begrenzten Kontextes, indem er das Konzept der Ubiquitous Language einführte.
Um die Bedeutung von Ubiquitous Language zu verstehen, ist es wichtig zu wissen, dass ein gut definiertes Domänenmodell in erster Linie ein gemeinsames Vokabular mit definierten Begriffen und Konzepten bereitstellt, eine gemeinsame Sprache zur Beschreibung der Domäne, die Fachexperten und Ingenieure in enger Zusammenarbeit entwickeln und dabei die geschäftlichen Anforderungen und Implementierungsüberlegungen abwägen. Diese gemeinsame Sprache, das gemeinsame Vokabular, nennen wir in DDD Ubiquitous Language. Die Bedeutung dieser Feststellung liegt darin, dass dieselben Wörter in verschiedenen Kontexten unterschiedliche Bedeutungen haben können. Ein klassisches Beispiel dafür ist in Abbildung 4-1 dargestellt. Der Begriff Konto hat in den Kontexten Identitäts- und Zugangsmanagement, Kundenmanagement und Finanzbuchhaltung eines Online-Reservierungssystems eine ganz andere Bedeutung.
In einem Identitäts- und Zugriffsmanagement-Kontext ist ein Konto ein Satz von Anmeldeinformationen, die für die Authentifizierung und Autorisierung verwendet werden. In einem Kundenmanagement-Kontext ist ein Konto eine Reihe von demografischen und Kontaktattributen, während es in einem Finanzbuchhaltungskontext wahrscheinlich Zahlungsinformationen und eine Liste vergangener Transaktionen ist. Wir sehen, dass dasselbe englische Grundwort in verschiedenen Kontexten mit deutlich unterschiedlicher Bedeutung verwendet wird. Das ist in Ordnung, denn wir müssen uns nur auf die allgegenwärtige Bedeutung der Begriffe (die Ubiquitous Language) innerhalb des begrenzten Kontexts eines bestimmten Domänenmodells einigen. Nach der DDD können wir die Grenzen der Kontexte erkennen, indem wir die Kanten beobachten, über die die Begriffe ihre Bedeutung ändern.
In DDD werden nicht alle Begriffe, die dir bei der Diskussion eines Domänenmodells in den Sinn kommen, in die entsprechende Ubiquitous Language aufgenommen. Konzepte in einem Bounded Context, die für den Hauptzweck des Kontexts von zentraler Bedeutung sind, sind Teil der Ubiquitous Language des Teams, alle anderen sollten ausgelassen werden. Diese Kernkonzepte kannst du in den JTBDs finden, die du für den Bounded Context erstellst. Schauen wir uns als Beispiel Abbildung 4-2 an.
In diesem Beispiel verwenden wir das in Kapitel 3 vorgestellte Job Story-Format und wenden es auf einen Auftrag aus dem Kontext der Identitäts- und Zugriffskontrolle an. Wir sehen, dass die in Abbildung 4-2 hervorgehobenen Schlüsselwörter den Begriffen in der zugehörigen Ubiquitous Language entsprechen. Wir empfehlen dringend, die Schlüsselwörter aus gut geschriebenen Aufträgen zu verwenden, um die für deine Ubiquitous Language relevanten Begriffe zu identifizieren.
Nachdem wir nun einige Schlüsselkonzepte von DDD erörtert haben, wollen wir uns etwas ansehen, das für die richtige Gestaltung von Microservice-Interaktionen sehr nützlich sein kann: das Context Mapping. Im nächsten Abschnitt von werden wir die wichtigsten Aspekte des Context Mappings untersuchen.
Context Mapping
In DDD versuchen wir nicht, ein komplexes System mit einem einzigen Domänenmodell zu beschreiben. Vielmehr entwerfen wir mehrere unabhängige Modelle, die im System koexistieren. Diese Subdomänen kommunizieren in der Regel über veröffentlichte Schnittstellenbeschreibungen miteinander. Die Darstellung der verschiedenen Domänen in einem größeren System und die Art und Weise, wie sie miteinander zusammenarbeiten, wird als Kontextkarte bezeichnet. Folglich wird der Akt der Identifizierung und Beschreibung dieser Kooperationen als Kontextabbildung bezeichnet, wie in Abbildung 4-3 dargestellt.
DDD identifiziert mehrere Haupttypen von Kooperationsinteraktionen bei der Abbildung von begrenzten Kontexten. Die grundlegendste Form ist der so genannte gemeinsame Kern. Er entsteht, wenn zwei Domänen weitgehend unabhängig voneinander entwickelt werden und sich am Ende - fast zufällig - in einer Teilmenge der Domäne des anderen überschneiden (siehe Abbildung 4-4). Zwei Parteien können sich darauf einigen, an diesem gemeinsamen Kernel zusammenzuarbeiten, der auch einen gemeinsamen Code und ein gemeinsames Datenmodell sowie eine gemeinsame Beschreibung der Domäne umfassen kann.
Auch wenn es auf den ersten Blick verlockend klingt (schließlich ist der Wunsch nach Zusammenarbeit einer der menschlichsten Instinkte), ist der gemeinsame Kernel ein problematisches Muster, vor allem wenn er für Microservices-Architekturen verwendet wird. Ein geteilter Kernel erfordert per Definition ein hohes Maß an Koordination zwischen zwei unabhängigen Teams, um die Beziehung überhaupt zu starten, und erfordert auch weiterhin Koordination für alle weiteren Änderungen. Wenn du deine Microservices-Architektur mit gemeinsam genutzten Kerneln ausstattest, entstehen viele Punkte, die eine enge Koordination erfordern. Wenn du einen gemeinsamen Kernel in einem Microservices-Ökosystem verwenden musst, ist es ratsam, dass ein Team als Haupteigentümer/Kurator bestimmt wird und alle anderen mitarbeiten.
Alternativ können zwei begrenzte Kontexte eine Beziehung eingehen, die DDD als Upstream-Downstream-Beziehung bezeichnet. Bei dieser Art von Beziehung fungiert der Upstream als Anbieter einer bestimmten Fähigkeit und der Downstream als Nachfrager dieser Fähigkeit. Da sich die Definitionen und Implementierungen der Domänen nicht überschneiden, ist diese Art von Beziehung lockerer gekoppelt als ein gemeinsamer Kern (siehe Abbildung 4-5).
Je nach Art der Koordination und Kopplung kann ein Upstream-Downstream-Mapping in verschiedenen Formen eingeführt werden:
- Kunde-Lieferant
-
In einem Kunden-Lieferanten-Szenario stellt der Upstream (Lieferant) dem Downstream (Kunden) Funktionen zur Verfügung. Solange die bereitgestellte Funktionalität wertvoll ist, sind alle zufrieden, aber Upstream trägt die Last der Rückwärtskompatibilität. Wenn der Upstream seinen Dienst ändert, muss er sicherstellen, dass der Kunde nichts davon erfährt. Noch dramatischer: Der Downstream (Kunde) trägt das Risiko, dass der Upstream absichtlich oder unabsichtlich etwas für ihn kaputt macht oder die zukünftigen Bedürfnisse des Kunden ignoriert.
- Konformist
-
Ein Extremfall der Risiken für eine Kunden-Lieferanten-Beziehung ist die konformistische Beziehung. Dabei handelt es sich um eine Abwandlung der Upstream-Downstream-Beziehung, bei der sich der Upstream ausdrücklich nicht um die Bedürfnisse seines Downstreams kümmert oder kümmern kann. Es ist eine Beziehung, die man auf eigenes Risiko nutzt. Der Upstream stellt eine wertvolle Fähigkeit zur Verfügung, die der Downstream nutzen möchte, aber da der Upstream nicht auf seine Bedürfnisse eingeht, muss sich der Downstream ständig an die Veränderungen im Upstream anpassen.
In großen Organisationen und Systemen treten häufig konforme Beziehungen auf, wenn ein viel größeres Teilsystem von einem kleineren genutzt wird. Stell dir vor, du entwickelst eine kleine, neue Funktion in einem Reservierungssystem einer Fluggesellschaft und musst dafür zum Beispiel ein unternehmensweites Zahlungssystem nutzen. Es ist unwahrscheinlich, dass ein so großes Unternehmenssystem einer kleinen, neuen Initiative den Vortritt lässt, aber du kannst auch nicht einfach ein ganzes Zahlungssystem im Alleingang neu implementieren. Entweder musst du dich anpassen, oder eine andere praktikable Lösung kann darin bestehen, getrennte Wege zu gehen. Letzteres bedeutet nicht immer, dass du ähnliche Funktionen selbst implementieren wirst. So etwas wie ein Zahlungssystem ist so komplex, dass kein kleines Team es nebenbei implementieren sollte, aber vielleicht kannst du dich außerhalb der Grenzen deines Unternehmens bewegen und stattdessen einen kommerziellen Zahlungsanbieter nutzen, wenn dein Unternehmen das zulässt.
Neben der Möglichkeit, sich anzupassen oder getrennte Wege zu gehen, hat der Downstream noch ein paar weitere DDD-sanktionierte Möglichkeiten, sich vor der Nachlässigkeit seines Upstreams zu schützen: eine Anti-Korruptionsschicht und die Verwendung von Upstreams, die offene Host-Schnittstellen anbieten.
- Anti-Korruptions-Ebene
-
In diesem Szenario erstellt der Downstream eine Übersetzungsschicht, die so genannte Anti-Korruptionsschicht (ACL), zwischen seiner und der Ubiquitous Language des Upstreams, um sich vor zukünftigen Änderungen an der Schnittstelle des Upstreams zu schützen. Die Erstellung einer ACL ist eine wirksame und manchmal notwendige Schutzmaßnahme, aber die Teams sollten bedenken, dass die Pflege dieser Schicht für den Downstream auf lange Sicht ziemlich teuer werden kann (siehe Abbildung 4-6).
- Offener Host-Dienst
-
Wenn der Upstream weiß, dass mehrere Downstreams seine Fähigkeiten nutzen könnten, sollte er, anstatt zu versuchen, die Bedürfnisse seiner vielen aktuellen und zukünftigen Kunden zu koordinieren, stattdessen eine Standardschnittstelle definieren und veröffentlichen, die alle Kunden übernehmen müssen. in DDD werden solche Upstreams als Open Host Services bezeichnet. Indem er ein offenes, einfaches Protokoll bereitstellt, in das sich alle autorisierten Parteien integrieren können, und die Rückwärtskompatibilität dieses Protokolls aufrechterhält oder eine klare und sichere Versionierung dafür bereitstellt, kann der Open Host seinen Betrieb ohne großes Drama skalieren. Praktisch alle öffentlichen Dienste (APIs) verwenden diesen Ansatz. Wenn du z. B. die APIs eines öffentlichen Cloud-Providers (AWS, Google, Azure usw.) nutzt, kennen sie dich in der Regel nicht und sind auch nicht speziell auf dich zugeschnitten, da sie Millionen von Kunden haben, aber sie sind in der Lage, einen nützlichen Dienst anzubieten und weiterzuentwickeln, indem sie als offener Host arbeiten (siehe Abbildung 4-7).
Zusätzlich zu den Relationstypen zwischen Domänen können Kontext-Mappings auch auf der Grundlage der verwendeten Integrationstypen zwischen begrenzten Kontexten differenzieren.
Synchrone versus asynchrone Integration
Integrationsschnittstellen zwischen begrenzten Kontexten können synchron oder asynchron sein, wie in Abbildung 4-8 dargestellt. Keines der Integrationsmuster geht grundsätzlich von dem einen oder dem anderen Stil aus.
Gängige Muster für synchrone Integrationen zwischen Kontexten sind RESTful APIs, die über HTTP bereitgestellt werden, gRPC-Dienste, die Binärformate wie Protobuf verwenden, und neuerdings auch Dienste, die GraphQL-Schnittstellen nutzen.
Auf der asynchronen Seite sind die Publish-Subscribe-Interaktionen führend. Bei diesem Interaktionsmuster kann der Upstream Ereignisse erzeugen und die Downstream-Dienste haben Worker, die diese verarbeiten können und wollen, wie in Abbildung 4-8 dargestellt.
Publish-Subscribe-Interaktionen sind komplexer zu implementieren und zu debuggen, aber sie bieten ein höheres Maß an Skalierbarkeit, Ausfallsicherheit und Flexibilität, da mehrere Empfänger, selbst wenn sie mit heterogenen Tech-Stacks implementiert sind, dieselben Ereignisse mit einem einheitlichen Ansatz und einer einheitlichen Implementierung abonnieren können.
Um die Diskussion über die Schlüsselkonzepte des Domain-Driven Design abzuschließen, sollten wir das Konzept der Aggregate untersuchen. Wir besprechen es im nächsten Abschnitt.
A DDD Aggregat
In DDD ist ein Aggregat eine Sammlung von verwandten Domänenobjekten, die von externen Konsumenten als eine Einheit betrachtet werden können. Diese externen Verbraucher verweisen nur auf eine einzige Einheit im Aggregat, und diese Einheit wird in DDD als Aggregatwurzel bezeichnet. Aggregate ermöglichen es Domänen, die interne Komplexität einer Domäne zu verbergen und nur die Informationen und Fähigkeiten (Schnittstelle) offenzulegen, die für externe Verbraucher "interessant" sind. Bei den bereits erwähnten Upstream-Downstream-Mappings muss der Downstream beispielsweise nicht jedes einzelne Domänenobjekt des Upstreams kennen und will dies in der Regel auch gar nicht. Stattdessen betrachtet er den Upstream als ein Aggregat oder eine Sammlung von Aggregaten.
Der Begriff Aggregat wird im nächsten Abschnitt wieder auftauchen, wenn wir Event Storming besprechen - eine leistungsstarke Methode, die den Prozess der fachlichen Analyse erheblich rationalisieren und zu einer viel schnelleren und unterhaltsameren Übung machen kann.
Einführung in Event Storming
Domain-Driven Design ist eine leistungsfähige Methode, um sowohl die Ebene des Gesamtsystems (in DDD "strategisch" genannt) als auch die detaillierte Zusammensetzung (in DDD "taktisch" genannt) deiner großen, komplexen Systeme zu analysieren. Wir haben auch gesehen, dass die DDD-Analyse uns dabei helfen kann, ziemlich autonome Teilkomponenten zu identifizieren, die über begrenzte Kontexte ihrer jeweiligen Domänen lose gekoppelt sind.
Es ist sehr einfach, zu dem Schluss zu kommen, dass wir, um zu lernen, wie man Microservices richtig dimensioniert, nur richtig gut in der fachlichen Analyse werden müssen. Wenn wir unser gesamtes Unternehmen dazu bringen, es ebenfalls zu lernen und sich darin zu verlieben (denn DDD ist sicherlich ein Teamsport), sind wir auf dem Weg zum Erfolg!
In den Anfängen der Microservices-Architekturen wurde DDD so allgemein als der einzig wahre Weg zur Dimensionierung von Microservices verkündet, dass der Aufstieg der Microservices auch der Praxis von DDD einen enormen Auftrieb gab - oder zumindest wurden sich mehr Menschen dessen bewusst und verwiesen darauf. Plötzlich sprachen viele Rednerinnen und Redner auf allen möglichen Softwarekonferenzen über DDD, und viele Teams gaben an, es in ihrer täglichen Arbeit einzusetzen. Bei genauerem Hinsehen stellte sich jedoch schnell heraus, dass die Realität etwas anders aussah und dass DDD zu einem der Dinge geworden war, über die viel geredet wird, die aber nicht praktiziert werden.
Versteh uns nicht falsch: Es gab Leute, die DDD schon lange vor Microservices genutzt haben, und es gibt auch heute noch viele, die es nutzen. Aber was die Nutzung von DDD als Tool für die Dimensionierung von Microservices angeht, war es mehr Hype und Vaporware als Realität.
Es gibt vor allem zwei Gründe, warum mehr Menschen über DDD gesprochen als es ernsthaft praktiziert haben: Es ist komplex und es ist teuer. DDD zu praktizieren, erfordert eine Menge Wissen und Erfahrung. Das Originalbuch von Eric Evans zu diesem Thema umfasst stolze 520 Seiten, und du müsstest mindestens noch ein paar weitere Bücher lesen, um es wirklich zu verstehen, ganz zu schweigen von der Erfahrung, die du bei der Umsetzung in einigen Projekten gesammelt hast. Es gab einfach nicht genug Leute mit den nötigen Fähigkeiten und Erfahrungen und die Lernkurve war steil.
Erschwerend kommt hinzu, dass DDD, wie bereits erwähnt, ein Teamsport ist, und zwar ein zeitaufwändiger. Es reicht nicht aus, eine Handvoll Technologen zu haben, die sich mit DDD auskennen. Du musst auch deine Geschäfts-, Produkt-, Design- usw. Teams davon überzeugen, an langen und intensiven Domain-Design-Sitzungen teilzunehmen, ganz zu schweigen davon, ihnen zumindest die Grundlagen dessen zu erklären, was du erreichen willst. Ist es das im Großen und Ganzen wert? Sehr wahrscheinlich ja: Vor allem bei großen, riskanten und teuren Systemen kann DDD viele Vorteile haben. Wenn du aber nur schnell ein paar Microservices einrichten willst und dein politisches Kapital auf der Arbeit bereits verspielt hast, indem du allen die neue Sache namens Microservices verkauft hast - dann hast du Glück, dass du von einer ganzen Reihe beschäftigter Leute verlangen kannst, dass sie dir genug Zeit geben, um deine Services richtig zu dimensionieren! Das ging einfach nicht - zu teuer und zu zeitaufwändig.
Und dann fand plötzlich ein Kollege namens Alberto Brandolini, der jahrzehntelang daran gearbeitet hatte, bessere Wege für die Zusammenarbeit von Teams zu finden, eine Abkürzung! Er schlug ein unterhaltsames, leichtgewichtiges und kostengünstiges Verfahren namens Event Storming vor, das stark auf den Konzepten von DDD basiert und von ihnen inspiriert ist, dir aber helfen kann, begrenzte Zusammenhänge in wenigen Stunden statt in Wochen oder Monaten zu finden. Die Einführung von Event Storming war ein Durchbruch für die kostengünstige Anwendbarkeit von DDD speziell für das Service Sizing. Natürlich ist es kein vollständiger Ersatz und bietet nicht alle Vorteile der formalen DDD (sonst wäre es ja Magie). Aber was die Entdeckung von begrenzten Kontexten angeht, ist es bei guter Annäherung in der Tat magisch!
Event Storming ist eine hocheffiziente Übung, die dabei hilft, begrenzte Kontexte einer Domäne auf eine schlanke, unterhaltsame und effiziente Art und Weise zu identifizieren, in der Regel viel schneller als mit traditioneller, vollständiger DDD. Es ist ein pragmatischer Ansatz, der die Kosten für DDD-Analysen so weit senkt, dass sie auch in Situationen durchführbar sind, in denen DDD sonst nicht bezahlbar wäre. Schauen wir uns an, wie diese "Magie" des Event Storming tatsächlich ausgeführt wird.
Der Event-Storming-Prozess
Die Schönheit von Event Storming liegt in seiner genialen Einfachheit. In physischen Räumen (wenn möglich, bevorzugt) brauchst du für eine Event Storming-Sitzung nur eine sehr lange Wand (je länger, desto besser), ein paar Hilfsmittel, vor allem Haftnotizen und Sharpies, und vier bis fünf Stunden Zeit von gut vertretenen Mitgliedern deines Teams. Für ein erfolgreiches Event Storming ist es wichtig, dass die Teilnehmer nicht nur Ingenieure sind. Eine breite Beteiligung von Gruppen wie Produkt-, Design- und Geschäftsinteressierten macht einen großen Unterschied. Du kannst auch virtuelle Event Storming-Sitzungen veranstalten, indem du digitale Tools für die Zusammenarbeit verwendest, die den hier beschriebenen physischen Prozess imitieren können.
Die Durchführung von Event Storming-Sitzungen beginnt mit dem Kauf der Materialien. Um die Sache zu vereinfachen, haben wir eine Amazon-Einkaufsliste erstellt, die wir für Event Storming-Sitzungen verwenden (siehe Abbildung 4-9). Sie besteht aus:
-
Eine große Anzahl von Stickies in verschiedenen Farben, vor allem orange und blau, und dann noch einige andere Farben für verschiedene Objekttypen. Davon brauchst du eine Menge. (Die Läden hatten nie genug für mich, also habe ich mir angewöhnt, online zu kaufen).
-
Eine Rolle 1/2-Zoll weißes Künstlerband.
-
Eine lange Papierrolle (z.B. Mala-Zeichenpapier von IKEA), die wir mit dem Klebeband an der Wand befestigen werden. Lege mehrere "Bahnen" an.
-
Mindestens so viele Sharpies wie die Anzahl der Teilnehmer/innen. Jeder muss seinen eigenen haben!
-
Haben wir schon erwähnt, dass wir eine lange, ungehinderte Wand brauchen, an der wir die Papierrolle festkleben können?
Bei Event Storming-Sitzungen ist eine breite Beteiligung, z. B. von Fachexperten, Produktverantwortlichen und Interaktionsdesignern, sehr wertvoll. Event Storming-Sitzungen sind kurz genug (nur ein paar Stunden statt tage- oder wochenlanger Analysen), dass sie in Anbetracht des Wertes ihrer Ergebnisse, der Klarheit, die sie für alle vertretenen Gruppen bringen, und der Zeit, die sie langfristig sparen, für alle Beteiligten gut investierte Zeit sind. Eine Event Storming-Sitzung, die sich nur auf Softwareentwickler/innen beschränkt, ist meist nutzlos, da sie in einer Blase stattfindet und nicht zu den funktionsübergreifenden Gesprächen führen kann, die für die gewünschten Ergebnisse notwendig sind.
Sobald wir die Materialien, den großen Raum mit einer offenen Wand und einer Rolle Papier, die wir mit Klebeband befestigt haben, und alle benötigten Personen haben, bitten wir (der Moderator) jeden, sich einen Haufen orangefarbener Klebezettel und einen persönlichen Sharpie zu schnappen. Dann geben wir ihnen eine einfache Aufgabe: Sie sollen die Schlüsselereignisse des untersuchten Bereichs auf orangefarbene Haftnotizen schreiben (ein Ereignis pro Notiz), ausgedrückt in einem Verb in der Vergangenheit, und die Notizen entlang eines Zeitstrahls auf dem an der Wand befestigten Papier platzieren, um eine "Zeitspur" zu erstellen, wie inAbbildung 4-10 dargestellt.
Die Teilnehmer/innen sollten sich nicht um die genaue Reihenfolge der Ereignisse kümmern, und in dieser Phase sollte es keine Koordination der Ereignisse unter den Teilnehmer/innen geben. Das Einzige, worum sie gebeten werden, ist, sich so viele Ereignisse wie möglich zu merken und die Ereignisse, die ihrer Meinung nach zu einem früheren Zeitpunkt stattfinden, nach links und die späteren Ereignisse nach rechts zu setzen. Es ist nicht ihre Aufgabe, Duplikate auszusortieren. Zumindest jetzt noch nicht. Diese Phase der Aufgabe dauert in der Regel 30 Minuten bis eine Stunde, je nach Größe des Problems und der Anzahl der Teilnehmer/innen. In der Regel solltest du mindestens 100 Ereignis-Zettel erstellt haben, bevor du die Aufgabe als erfolgreich bezeichnen kannst.
In der zweiten Phase der Übung wird die Gruppe gebeten, die entstandenen Notizen an der Wand zu betrachten und sie mit Hilfe des Moderators zu einer kohärenteren Zeitleiste zu ordnen, indem sie Duplikate identifiziert und entfernt. Wenn genügend Zeit zur Verfügung steht, ist es für die Teilnehmer/innen sehr hilfreich, eine "Storyline" zu erstellen, in der sie die Ereignisse in einer Reihenfolge durchgehen, die eine Art "User Journey" darstellt. In dieser Phase kann es sein, dass das Team Fragen oder Unklarheiten hat. Wir versuchen nicht, diese Probleme zu lösen, sondern halten sie als "Hotspots" fest - verschiedenfarbige Klebezettel (meist lila), auf denen die Fragen stehen. Die Hotspots müssen offline in Folgegesprächen beantwortet werden. Diese Phase kann ebenfalls 30 bis 60 Minuten in Anspruch nehmen.
In der dritten Phase erstellen wir das, was im Event Storming als Rückwärtserzählung bezeichnet wird. Dabei gehen wir die Zeitachse vom Ende zum Anfang zurück und identifizieren die Befehle, also die Dinge, die die Ereignisse verursacht haben. Für die Befehle verwenden wir Haftnotizen in einer anderen Farbe (normalerweise blau). In diesem Stadium sieht dein Storyboard vielleicht so aus wie in Abbildung 4-11.
Sei dir bewusst, dass viele Befehle eine eins-zu-eins-Beziehung zu einem Ereignis haben werden. Es wird sich redundant anfühlen, wie dasselbe in der Vergangenheit und in der Gegenwart zu sagen. Wenn du dir die vorherige Abbildung ansiehst, sind die ersten beiden Befehle tatsächlich so. Das verwirrt oft Leute, die neu im Event Storming sind. Ignoriere es einfach! Wir fällen beim Event Storming keine Urteile, und während einige Befehle 1:1 mit den Ereignissen übereinstimmen können, ist das bei anderen nicht der Fall. Zum Beispiel löst der Befehl "Zahlungsgenehmigung einreichen" eine ganze Reihe von Ereignissen aus. Halte einfach fest, was du weißt und denkst, dass es im echten Leben passiert, und mach dir keine Gedanken darüber, wie du es "hübsch" oder "ordentlich" machen kannst. Die reale Welt, die du modellierst, ist normalerweise auch chaotisch.
In der nächsten Phase erkennen wir an, dass Befehle nicht direkt Ereignisse erzeugen. Stattdessen nehmen spezielle Arten von Domänenentitäten Befehle an und erzeugen Ereignisse. Im Event Storming werden diese Entitäten Aggregate genannt (ja, der Name ist von dem ähnlichen Konzept in DDD inspiriert). In dieser Phase ordnen wir unsere Befehle und Ereignisse neu an und unterbrechen die Zeitachse, wenn nötig, so dass die Befehle, die an dasselbe Aggregat gehen, um dieses Aggregat herum gruppiert werden und die von diesem Aggregat "ausgelösten" Ereignisse ebenfalls dorthin verschoben werden. Ein Beispiel für diese Phase des Event Storming siehst du in Abbildung 4-12.
Diese Phase der Übung kann 15 bis 25 Minuten dauern. Wenn wir damit fertig sind, solltest du feststellen, dass unsere Wand jetzt weniger wie eine Zeitleiste von Ereignissen aussieht, sondern eher wie eine Ansammlung von Ereignissen und Befehlen, die um Aggregate gruppiert sind.
Weißt du was? Diese Cluster sind die begrenzten Kontexte, nach denen wir gesucht haben.
Das Einzige, was bleibt, ist, die verschiedenen Kontexte nach ihrer Priorität zu klassifizieren (ähnlich wie "root", "supportive" und "generic" in DDD). Dazu erstellen wir eine Matrix von begrenzten Kontexten/Unterbereichen und ordnen sie nach zwei Eigenschaften ein: Schwierigkeit und Wettbewerbskanten. In jeder Kategorie verwenden wir die T-Shirt-Größen <S, M oder L>, um sie entsprechend einzustufen. Die Entscheidung darüber, wann wir uns anstrengen sollten, basiert auf den folgenden Richtlinien:
-
Großer Wettbewerbsvorteil/großer Aufwand: Dies sind die Kontexte, die intern entwickelt und umgesetzt werden müssen und für die man am meisten Zeit aufwendet.
-
Kleiner Vorteil/großer Aufwand: Kaufen!
-
Kleiner Vorteil/kleiner Aufwand: tolle Aufgaben für Auszubildende.
-
Andere Kombinationen sind ein Münzwurf und erfordern eine Ermessensentscheidung.
Hinweis
Diese letzte Phase, die "Wettbewerbsanalyse", ist nicht Teil des ursprünglichen Event Storming-Prozesses von Brandolini und wurde von Greg Young für die Priorisierung von Domänen in DDD im Allgemeinen vorgeschlagen. Wir finden, dass sie eine nützliche und unterhaltsame Übung ist, wenn sie mit einer angemessenen Portion Humor durchgeführt wird.
Der gesamte Prozess ist sehr interaktiv, erfordert die Beteiligung aller Teilnehmenden und macht in der Regel auch Spaß. Es braucht einen erfahrenen Moderator, um die Dinge reibungslos in Gang zu halten, aber die gute Nachricht ist, dass ein guter Moderator zu sein, nicht den gleichen Aufwand erfordert wie ein Raketenwissenschaftler (oder DDD-Experte) zu werden. Nach der Lektüre dieses Buches und der Durchführung einiger Probesitzungen zur Übung kannst du ganz einfach ein Weltklasse-Event-Storming-Moderator werden!
Als Moderator/in ist es eine gute Idee, auf die Zeit zu achten und einen Plan für deine Sitzung zu haben. Für eine vierstündige Sitzung könnte die grobe Zeiteinteilung so aussehen:
-
Phase 1 (~30 min): Domänenereignisse entdecken
-
Phase 2 (~45 min): Den Zeitplan durchsetzen
-
Phase 3 (~60 min): Umkehrung der Erzählung und Identifizierung der Befehle
-
Phase 4 (~30 min): Aggregate/begrenzte Kontexte identifizieren
-
Phase 5 (~15 min): Wettbewerbsanalyse
Und wenn du feststellst, dass diese Zeiten nicht 4 Stunden ergeben, dann denke daran, dass du den Leuten in der Mitte eine Pause gönnen und dir selbst Zeit lassen solltest, um den Raum vorzubereiten und am Anfang anzuleiten.
Einführung der Universal Sizing Formula
Bounded Contexts sind ein fantastischer Ausgangspunkt für das Rightsizing von Microservices. Wir müssen jedoch vorsichtig sein und dürfen nicht davon ausgehen, dass Microservice-Grenzen gleichbedeutend mit den Bounded Contexts aus DDD oder Event Storming sind. Das sind sie nicht. Tatsächlich kann man nicht davon ausgehen, dass Microservice-Grenzen im Laufe der Zeit konstant sind. Sie entwickeln sich im Laufe der Zeit und folgen in der Regel einer zunehmenden Granularität der Microservices, wenn die Organisationen und Anwendungen, zu denen sie gehören, reifen. Adrian Cockroft stellte zum Beispiel fest, dass dies definitiv ein sich wiederholender Trend ist, den er während seiner Zeit bei Netflix beobachtet hat.
Niemand bekommt Microservice-Grenzen von Anfang an perfekt hin
In erfolgreichen Fällen der Einführung von Microservices beginnen die Teams nicht mit Hunderten von Microservices. Sie beginnen mit einer viel geringeren Anzahl, die sich eng an begrenzten Kontexten orientiert. Im Laufe der Zeit teilen Teams Microservices auf, wenn sie auf Koordinationsabhängigkeiten stoßen, die sie beseitigen müssen. Das bedeutet auch, dass von den Teams nicht erwartet wird, dass sie die Dienstgrenzen von Anfang an "richtig" festlegen. Stattdessen entwickeln sich die Grenzen im Laufe der Zeit weiter, mit der allgemeinen Tendenz zu mehr Granularität.
Es ist erwähnenswert, dass es in der Regel einfacher ist, einen Dienst aufzuteilen als mehrere Dienste wieder zusammenzuführen oder eine Fähigkeit von einem Dienst in einen anderen zu verschieben. Das ist ein weiterer Grund, warum wir empfehlen, mit einem grobkörnigen Design zu beginnen und zu warten, bis wir mehr über die Domäne erfahren und genug Komplexität haben, bevor wir die Dienste aufteilen und die Granularität erhöhen.
Wir haben festgestellt, dass es drei Prinzipien gibt, die gut zusammen funktionieren, wenn man über die Granularität von Microservices nachdenkt. Wir nennen diese Prinzipien die Universal Sizing Formula für Microservices.
Die Universal-Größenformel
Um ein vernünftiges Sizing von Microservices zu erreichen, solltest du:
-
Beginne mit einigen wenigen Microservices, eventuell unter Verwendung von Bounded Contexts.
-
Teile weiter auf, wenn deine Anwendung und deine Dienste wachsen, und richte dich dabei nach den Erfordernissen der Koordinationsvermeidung.
-
Sei auf dem richtigen Weg, um die Koordination zu verringern. Das ist viel wichtiger als der aktuelle Stand von , wie "perfekt" du den Service bemessen kannst.
Zusammenfassung
In diesem Kapitel haben wir uns mit der kritischen Frage beschäftigt, wie man Microservices richtig dimensioniert. Wir haben uns mit Domain-Driven Design beschäftigt, einer beliebten Methode zur Modellierung der Dekomposition komplexer Systeme; wir haben erklärt, wie man mit der Event Storming-Methode eine hocheffiziente Domänenanalyse durchführt, und wir haben die Universal Sizing Formula vorgestellt, die eine einzigartige Anleitung für die effektive Dimensionierung vonMicroservices bietet.
In den folgenden Kapiteln gehen wir näher auf die Implementierung ein und zeigen, wie man Daten in einer lose gekoppelten, komponentenbasierten Microservices-Umgebung verwaltet. Außerdem führen wir dich durch eine Beispielimplementierung für unser Demoprojekt: ein Online-Reservierungssystem.
Get Microservices: Auf und davon now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.