… oder wie ich ein Haus baue, in dem ich auch wohnen möchte

Microservices oder Monolithen? Beides!

Microservices oder Monolithen? Beides!

… oder wie ich ein Haus baue, in dem ich auch wohnen möchte

Microservices oder Monolithen? Beides!


Seit 2012 und dem Artikel von James Lewis und Martin Fowler sind Microservices in aller Munde. Sie schienen die Antwort auf eine stets wachsende Komplexität und die Cloud zu sein. Aber mit der Zeit wurde es immer deutlicher, dass es Aufgabenstellungen gibt, die anders beantwortet werden müssen.

Der vorliegende Beitrag setzt sich mit den Vor- und Nachteilen von Microservices und Monolithen anhand von realen Beispielen auseinander. Als Ergebnis werden Kriterien vorgestellt, die diese komplexen architektonischen Entscheidungen vereinfachen können.

Microservices

Microservices sind eine Technik, die eine Applikation als eine Sammlung lose gekoppelter Dienste zusammenstellt [1]. Daher müssen die Dienste feingranular und die verwendeten Protokolle leichtgewichtig sein. In Abbildung 1 ist auf der linken Seite eine Microservices-Architektur schematisch dargestellt. Ein Microservice stellt in der Regel eine Funktion als einen Dienst zur Verfügung. Um Geschäftsanwendungen zu realisieren, müssen viele Dienste mit unterschiedlicher Funktion miteinander gekoppelt werden.

junker_microservices_1.tif_fmt1.jpgAbb. 1: Verschiedene Architekturansätze

Vorteile

Ein Microservice lässt sich als einzelner Dienst einfacher in die Produktion bringen. Da Microservices klein und abgeschlossen sind, lassen sie sich besser verstehen und können dadurch besser gewartet werden. Sie werden durch ein Team weiterentwickelt und gepflegt.

Nachteile

Da Microservices nur einzelne Funktionen zur Verfügung stellen, sind Geschäftsanwendungen notwendigerweise verteilte Systeme. Schnell kann man bei der Entwicklung von verteilten Systemen gewissen Trugschlüssen erliegen, da diese bei der Entwicklung nicht unmittelbar sichtbar sind [2]:

  • Das Netzwerk ist verlässlich.

  • Es gibt keine Latenzzeiten bei der Nachrichtenübertragung.

  • Das Netzwerk ist sicher.

  • Die Netzwerktopologie ändert sich nie.

  • Transportkosten von Nachrichten sind nicht vorhanden.

Mit solchen oft impliziten Annahmen landet man im Chaos. Diese Dinge müssen in der Grundarchitektur unbedingt berücksichtigt werden. Weitere Herausforderungen sind die verteilte Datenpersistenz und die damit verbundene Eventual Consistency [3] sowie das komplexere Ende-zu-Ende-Testen.

Microservices sind als einzelne Dienste einfach in die Produktion zu bringen. Allerdings müssen viele Abhängigkeiten für eine Gesamtapplikation beachtet werden. Sowohl das Einbringen in die Produktion als auch das Testen von Microservices-Architekturen müssen automatisiert werden, um die Komplexität beherrschen zu können.

Monolithen

Ein Monolith ist alles, was einheitlich und unbeweglich ist [4]. In der Softwarearchitektur verstehen wir unter Monolithen Softwarestrukturen, die in einem Dienst viele Funktionen zur Verfügung stellen, die nicht ohne Aufwand zu trennen sind. Abbildung 1 zeigt auf der rechten Seite eine monolithische Architektur, die sich schon entwickelt hat. Das heißt, dass einzelne Services entgegen dem ursprünglichen Entwurf hinzugefügt oder auch entfernt wurden.

Vorteile

Obwohl diese Nichttrennbarkeit als ein zusätzlicher Aufwand zu sehen ist, kann sie auch ein Vorteil sein, da Monolithen einfach in eine Produktion zu bringen sind, wenn sie nicht zu komplex sind. Daher lassen sie sich auch bis zu einem gewissen Maße horizontal skalieren. Sie sind einfach Ende-zu-Ende zu testen, wenn das Testen manuell erfolgt.

Nachteile

Da in der Regel viele Funktionalitäten in einem Monolith zusammengefasst sind, sind diese schwer zu verstehen und Änderungen lassen sich nicht einfach umsetzen. Wenn nur kleine Änderungen in die Produktion gebracht werden müssen – wie z. B. ein neuer Mehrwertsteuersatz –, muss die gesamte Funktionalität neu eingebracht und getestet werden. Auch wenn der Monolith sich einfach in die Produktion bringen lässt, sind Startzeiten in der Regel sehr lang, was eine Automatisierung verhindert.

Solche kleineren Änderungen sind in der Regel bei Monolithen wirkliche Alpträume, da Auswirkungen auf die einzelnen Funktionen nicht überschaubar sind. Die Trennung der einzelnen Funktionalitäten beruht auf Absprachen und lässt sich nicht technisch erzwingen.

Als Vorteil einer monolithischen Architektur wurde auch das einfache horizontale Skalieren genannt, aber Monolithen muss man immer als Ganzes skalieren. Das heißt, dass alle Funktionalitäten skaliert werden, auch wenn nur eine relativ kleine Funktionalität skaliert werden muss. Als Beispiel sei hier eine Log-in-Funktionalität genannt. Log-ins unterliegen häufig sehr unterschiedlichen Zugriffsraten. In einer Einkaufsanwendung gibt es zur Weihnachtszeit Spitzen, während im Spätsommer Langeweile herrscht. In einer monolithischen Anwendung wird der entsprechende Server am Ressourcenverbrauch in den Spitzenzeiten ausgerichtet, da Neuaufsetzen und automatisches Skalieren nur schwer umzusetzen sind. Wird die Log-in-Funktion herausgelöst, kann der Businessmonolith bestehen bleiben und die Log-in-Funktion als kleiner Dienst automatisch skaliert werden.

Irgendwas dazwischen – Modulith

Wenn man die zusätzliche Infrastrukturkomplexität einer Microservices-Architektur scheut, auf der anderen Seite aber eine pflegbare und verlässliche Anwendung benötigt, bietet sich ein Modulith an. Abbildung 1 zeigt in der Mitte beispielhaft einen solchen Modulithen. Obwohl er nach außen als einheitliches Artefakt auftritt, ist er im Inneren gut und sauber strukturiert.

Hierbei wird ein Modulith als Deployment-Monolith verstanden, wie er von Martin Fowler in „Monolith First“ [5] eingeführt wird. Ein Deployment-Monolith ist ein gut strukturierter Monolith, der als Ganzes in die Produktion gebracht wird. Der gut passende Begriff Modulith für einen solchen Deployment-Monolithen wurde von Herbert Dowalil [6] eingeführt.

Vorteile

Ein Modulith folgt dem YAGNI-Prinzip. YAGNI meint hier ein Mantra der agilen Softwareentwicklung: „You aren’t gonna need it”. Bezogen auf eine Microservices-Architektur heißt das, dass man es im ersten Schritt bleiben lassen soll, wenn man nicht weiß, ob man Microservices braucht und wo man die einzelnen Dienste schneiden soll. Trotzdem kann man die Software so strukturieren, dass man später einzelne Services herauslösen kann.

Im Java-Bereich unterstützen Java-Module dieses Vorgehen. Sie ermöglichen eine stärkere Strukturierung als Java-Packages und verhindern in der Regel die versehentliche Benutzung von modulfremden Klassen. Module in Java verfolgen hierbei die folgenden Ziele:

  • verlässliche Konfiguration,

  • strenge Kapselung der einzelnen Module und

  • eine bessere Skalierbarkeit der Java-Runtime-Plattform [7].

Nachteile

Problematisch an diesem Ansatz ist, dass die Kapselung der einzelnen Module und die Strukturierung entlang des Businessprozesses sehr schnell „zerfleddert“. Oft helfen hier nur organisatorische Maßnahmen – wie eine übergreifende statische Codeanalyse oder ein strenge Architekturreview durch ein übergreifendes Architekturgremium, um die angedachte Architektur durchzusetzen. Das heißt, dass man tief in die Teamautonomie eingreift. Mehr noch: Eine solche zerfledderte Architektur in kleine Dienste zu schneiden, ist nahezu unmöglich. Oft endet man hier bei einer Neuimplementierung der Gesamtapplikation.

Entwurf von Architekturen: funktionaler Schnitt

Als funktionaler Schnitt wird in diesem Beitrag der Schnitt von einzelnen Diensten verstanden, die bestimmte Geschäftsfunktionen implementieren. Parallel hierzu können unterstützende Funktionen wie z. B. der schon erwähnte Log-in-Service oder auch ein Berichtsdienst existieren. Alle diese Funktionen müssen getrennt voneinander implementiert werden. Dabei ist es im ersten Schritt nicht interessant, ob sie gemeinsam oder getrennt voneinander in Produktion gebracht werden.

Nichtfunktionale Anforderungen

Architektur wird benötigt, um nichtfunktionale Anforderungen zu implementieren. Oft werden sie „hinter dem Vorhang“ implementiert. Das heißt, dass funktionale Anforderungen durch die Produktteams implementiert werden, aber übergreifende Anforderungen hinsichtlich Performanz, Sicherheit, Verlässlichkeit, Resilienz und Pflegbarkeit durch eine übergreifende Architektur sichergestellt werden müssen. Insbesondere die nichtfunktionalen Anforderungen Pflegbarkeit und Skalierbarkeit werden durch Microservices-Architekturen adressiert. Während die nichtfunktionalen Anforderungen Sicherheit und Resilienz durch eine Microservices-Architektur eine verstärkte Aufmerksamkeit verlangen.

Organisatorische Anforderungen

Komplexere Businessanwendungen, wie z. B. eine Einkaufslösung oder auch eine Produktionsplanung, werden in der Regel durch mehrere Teams innerhalb eines Projekts implementiert. Um Teamunabhängigkeit hinsichtlich Technologieauswahl und Produktionsgang zu ermöglichen, sind Microservices, die innerhalb eines Teams liegen, unabdingbar.

Entscheidungskriterien

In Tabelle 1 sind Entscheidungskriterien für die verschiedenen Architekturansätze dargestellt. Mit fachlicher Komplexität ist hier die Komplexität des Geschäftsprozesses gemeint. Ein einfacher Geschäftsprozess verfügt über einen oder zwei Schritte. Ein Beispiel wäre ein Reservierungsvorgang in einem Restaurant:

  • nehme Reservierungswunsch auf

  • prüfe Verfügbarkeit des Tisches für die Anzahl der Personen

  • passe Verfügbarkeit an

Kriterium

Monolith

Modulith

Microservices

Fachliche Komplexität

Gering

Mittel

Hoch

Funktionen mit unterschiedlichen Ressourcenanforderungen

Nein

Ja

Ja

Anzahl der implementierenden Teams

1

1 bis 3

4 und mehr

Verteilte Teams

Nein

Nein

Ja

Hohe Skalierbarkeit gefordert

Nein

Nein

Ja

Automatisches Testen notwendig

Nein

Nein

Ja

Automatisches Deployment notwendig

Nein

Nein

Ja

Tabelle 1: Entscheidungskriterien für einen Architekturansatz

Komplexe Geschäftsprozesse müssen mehrere Statusübergänge eines Geschäftsobjekts verwalten, wobei in der Regel die Statusübergänge von anderen Geschäftsprozessen angestoßen werden. Als Beispiel sei hier die Bestandsverwaltung einer Versicherung genannt, in der komplexe Produkte wie Lebensversicherungen verwaltet werden. Bei komplexen Geschäftsprozessen bietet sich eine Microservices-Architektur an, um die einzelnen Prozessschritte möglichst entkoppelt voneinander entwickeln zu können. Spätere Erweiterungen oder Änderungen im Geschäftsprozess lassen sich so einfacher einfügen.

Funktionen mit unterschiedlichen Ressourcenanforderungen sollten nicht in einem Produktionsgang zusammengebunden werden, da sie Automatisierung und Skalierung erschweren. Leichtgewichtige Funktionen, wie eine einfache Suche, können gut von schwergewichtigen Funktionen wie eine Produktverwaltung getrennt werden. Dann kann die Suche gut mit den Benutzeranforderungen skalieren und trotzdem der Gesamtressourcenverbrauch optimiert werden.

Große Geschäftsanwendungen werden immer mit mehreren Teams implementiert. Als Team werden hier Produktteams mit sieben bis zehn Mitgliedern verstanden. Wenn diese Teams möglichst selbstständig arbeiten sollen, muss man eine Microservices-Architektur anstreben. Ansonsten ist der Synchronisations- und Abstimmungsaufwand zwischen den Teams so hoch, dass die Zeit für Implementierungsaufgaben erheblich eingeschränkt wird. Bei kleineren Projekten mit nur einem oder zwei Teams liegt diese Einschränkung nicht vor. Allerdings muss auch hier die Software gut strukturiert sein, um den Abstimmungsaufwand auch innerhalb der Teams gering zu halten.

Auch mit wenigen Teams empfiehlt sich ein Modulith- oder sogar Monolithansatz nur, wenn die Teams nicht räumlich verteilt sind. Eine gute kollaborative Entwicklung funktioniert nur dann, wenn die Teams räumlich eng zusammenarbeiten können. Auch wenn das durch die Anwendung von Messaging und Videokonferenzsysteme abgemildert werden kann, sinkt die Implementierungsgeschwindigkeit bei verteilten Teams.

Je größer Monolith und Modulith werden, desto mehr sinkt ihre Fähigkeit, automatisch getestet und in die Produktion gebracht zu werden. Ist eine vollautomatische Produktivgangstrecke mit CD/CI (Continuous Delivery/Continuous Integration) und vollautomatisches Testen geplant, sollte man zeitig an eine Microservices-Architektur denken. Um das zu erreichen, müssen die einzelnen Dienste gut verstanden und beherrschbar sein. Daher ist ihre Größe limitiert.

junker_microservices_2.tif_fmt1.jpgAbb. 2: Matrix zur Einordnung von Skalierbarkeit und funktioneller Komplexität

Abbildung 2 zeigt die Abhängigkeit der unterschiedlichen Architekturansätze von Skalierbarkeit und funktioneller Komplexität. Steigt beides, sollte man eine Microservices-Architektur anstreben. Insbesondere gilt das für die Ablösung und Erweiterung von historisch gewachsenen Architekturen. Eine solche Ablösung und der Ersatz von Geschäftsanwendungen sind in jedem Fall komplex. Aus den gewachsenen Monolithen und auch den etwas jüngeren Modulithen müssen Schritt für Schritt die einzelnen Funktionalitäten als beherrschbare Dienste herausgelöst werden. Auch hier gilt, dass die einzelnen Funktionalitäten hinsichtlich der Notwendigkeit einer Microservices-Architektur bewertet werden müssen. Es entsteht eine hybride Architektur, wie sie in Abbildung 3 schematisch dargestellt ist.

junker_microservices_3.tif_fmt1.jpgAbb. 3: Schematische Darstellung einer hybriden Architektur

Zusammenfassung

Moderne Architekturen lassen sich als Microservices-Architekturen entwerfen. Allerdings muss die Komplexität der verteilten Applikation beherrschbar sein. Dieser zusätzliche Aufwand lässt sich durch Deployment-Monolithen oder auch Modulithen vermeiden. Sie können bis zu einer gewissen Komplexität und Projektgröße gut eingesetzt werden. Bei über mehrere Jahre laufenden Projekten entstehen in der Regel immer hybride Architekturen, für die aber die vorgestellten Entscheidungskriterien für die unterschiedlichen Architekturansätze ebenso angewendet werden können. Wendet man diese Kriterien an, entsteht ein Haus – egal ob Neu- oder Umbau –, in dem man auch wohnen möchte.

junker_annegret_dr_sw.tif_fmt1.jpgDr. Annegret Junker ist Principal Software Architect bei adesso SE. Sie arbeitet seit mehr als 25 Jahren in der Softwareentwicklung in unterschiedlichen Rollen und Domänen wie Automotive, Versicherungen und Finanzdienstleistungen. Besonders interessiert sie sich für DDD, Microservices und alles, was damit zusammenhängt. Derzeit arbeitet sie in einem großen Versicherungsprojekt als übergreifende Architektin.

Mail
Dr. Annegret Junker

Annegret Junker ist Chief Architect bei codecentric AG. Sie arbeitet seit mehr als 30 Jahren in der Software-Entwicklung in unterschiedlichen Rollen und unterschiedlichen Domänen wie Automotive, Versicherungen und Finanzdienstleistungen. Besonders interessiert sie sich für DDD, Microservices und alles, was damit zusammenhängt. Derzeit arbeitet sie in einem großen Integrationsprojekt und definiert APIs für Partner.


Weitere Artikel zu diesem Thema