Die Softwarelandschaft ist übersäht von serviceorientierten Architekturen. Monolithen gelten als “nicht mehr State of the Art” – Heute “macht man halt Microservices”. Außer Acht gelassen wird jedoch der Aspekt, dass man es richtig machen muss, um tatsächlich einen Nutzen aus dieser Architektur zu ziehen. Folgender Artikel gibt einen Überblick über die Qualitätskriterien, die dazu erfüllt werden sollten.
Während der Zusammenarbeit mit einem Kunden entstand innerhalb des ersten Jahres eine monolithische Struktur. Gerade zu Beginn hatte die Entwicklung in dieser Form viele Vorteile: Nur ein Projekt muss erstellt und deploy- sowie lauffähig gemacht werden. Auch lokal lässt sich die Anwendung fix starten, weil nur einer statt zahlreicher Services involviert sind. Zudem nutzt das gesamte Entwicklerteam denselben Tech-Stack und die gesamte Domäne ist zugreifbar.
Klingt In der Theorie super, und auch wenn Monolithen in der Praxis nicht per se eine unsaubere Architektur bedeuten erinnert das Ergebnis in der Realität meist leider doch an Pasta statt sauber getrennten Bereichen. Zwar stellten wir über Codestyle-Regeln sicher, dass die beinhalteten Domänen voneinander getrennt blieben, für eine parallele Entwicklung mit zwei Entwicklerteams reichte das jedoch nicht mehr aus: Mittelgroße Refactorings des einen Teams blockierten das andere, Kommunikation wie auch Absprachen fraßen unsere Zeit und bei Deployments konnte man entspannt einmal um den Block spazieren.
Für eine strikte Trennung der Teams entschieden wir uns dazu, den Monolithen in mehrere Teile zu zerlegen. Im Zuge dessen beschäftigten wir uns mit der Frage, welche Kriterien Microservices erfüllen sollten, um einen positiven Beitrag zur Softwarequalität zu leisten. Denn bei unbedachtem Vorgehen wird aus einem “Ball of mud” leider eine Gruppe verteilter “balls of mud”:
Generell muss die Entscheidung für eine Software-Architektur immer im Kontext des Anwendungsfalls getroffen werden. Soll beispielsweise innerhalb einiger Wochen ein Prototyp auf die Beine gestellt werden, ist das Investment in Microservices nicht die beste Idee. Fällt die Wahl begründet auf Microservices, weil beispielsweise die unabhängige Skalierung einzelner Teile der Applikation von Bedeutung ist, sollten bestimmte Kriterien berücksichtigt werden. Andernfalls wird die Komplexität eines verteilten Systems eingekauft, ohne im Gegenzug daraus Profit zu schlagen.
Zunächst muss die Komplexität des verteilten Systems bewältigt werden. Um eine schnelle und effiziente Entwicklung sicherzustellen, ist eine ausgereifte Infrastruktur unabdinglich. Diese umfasst unter anderem die Automatisierung des Systems auf Ebenen wie Delivery und Deployment, Load Balancing mit Service Discovery sowie Monitoring.
Nicht nur das Was, sondern ebenso das Wie ist ausschlaggebend, um zwischen den Services eine passende Abgrenzung zu treffen getroffen. Ziel ist hier die lose Kopplung, um möglichst wenig Abhängigkeiten zwischen den Services zu erhalten. Jeder einzelne Service soll zudem über hohe Kohäsion verfügen und demnach Funktionalität in Gänze umsetzen. Zur Abgrenzung der Services stellt das DDD eine ganze Kiste an Werkzeugen bereit.
Es genügt nicht nur, den Code in separate Repositories aufzuteilen. Ein System mit zehn getrennten Services kann noch immer einen Monolithen darstellen, wenn alles gemeinsam deployed werden muss. Anzustreben ist also nicht nur unabhängiger Code, sondern auch ein unabhängiges Deployment. Ebenso zu vermeiden sind Laufzeit-Monolithen. Passende Technologien Die getrennte Code-Basis und unabhängig lauffähige Applikationen machen es möglich, dem Verwendungszweck entsprechende Technologien zu nutzen. Diese sollte bedacht gewählt und die Gefahr des Technologiepluralismus gegengerechnet werden.
Je loser die Komponenten gekoppelt sind, desto weniger Kommunikation muss stattfinden. Komplett darauf verzichten lässt sich meist jedoch nicht. Dann gilt die “asynchron first”-Devise aus dem Reactive Manifesto, nach der Daten bevorzugt über Nachrichten ausgetauscht werden sollen. Das Manifest definiert zudem weitere Kriterien reaktiver Systeme: Antwortbereitschaft, Widerstandsfähigkeit, Elastizität (also die Anpassungsfähigkeit auf sich ändernde Lasten) sowie Wartbar- und Erweiterbarkeit. Im Grunde zahlen alle der Punkte auf die Reaktionsfähigkeit ein.
Ein zusammengesetztes System bietet natürlich zahlreiche Punkte, an denen etwas kaputt gehen kann. Hier sollte ein Auge auf der Fehlertoleranz liegen: Ist ein Service langsam oder gar ausgefallen, sollte der Rest des Systems weiter funktionieren. Der Chaos Monkey von Netflix zielt genau hierauf ab und setzt einzelne Komponenten außer Gefecht, um sich ausbreitende Fehlerquellen zu identifizieren. Denkbar wäre der Einsatz von Caching, um als Fallback-Mechanismus auf zuvor abgefragte Daten zurückzugreifen, oder der Einsatz von Circuit Breakern, um dem Nutzer eine schnellere Antwort zu liefern und im Gegenzug Regenerationszeit einzukaufen.
Ohne das Vorhandensein eines Zustands können ohne Probleme mehrere Instanzen eines Services gestartet werden. Zustandslosigkeit ist demnach ein anzustrebender Aspekt. Problematisch wird es jedoch, wenn mehrere Instanzen einen eigenen Zustand verwalten, der zudem synchronisiert werden muss.
Die Qualitätskriterien in Kürze:
Nicht alle der oben genannten Kriterien können zu jedem Zeitpunkt erfüllt werden, deren Berücksichtigung erhöht aber die Chance darauf, statt bei “distributed balls of mud” bei einer (mehr oder weniger) wohlgeformten Architektur zu landen:
Werden obige Punkte bei der Einleitung einer verteilten Architektur berücksichtigt, stehen die Chancen gut mit Microservices einen Zugewinn zu erlangen. Unser erster Schritt im anfangs erwähnten Projekt war das Heraustrennen eines ersten Microservices aus dem Monolithen. Dabei implementierten wir die Basis für eine serviceorientierte Architektur und stellten die Infrastruktur zur Kommunikation zwischen unabhängigen Komponenten bereit. Fortan wurden neue funktionale Bestandteile direkt als Microservices umgesetzt und Stück für Stück weitere Bereiche aus dem Monolithen herausgelöst.