Um die Vorteile und Herausforderungen von Microservices zu verstehen, muss der Begriff „Microservice“ definiert werden. Das bieten die ISA-Prinzipien (Independent Systems Architecture). Die wichtigsten Prinzipien sind:

Die ISA-Prinzipien definieren noch weitere Eigenschaften, die aber für diesen Artikel keine Rolle spielen. Microservices bieten durch diese Prinzipien Vorteile in verschiedenen Bereichen.

Organisatorische Vorteile

Der erste Bereich ist die Organisation. Das erscheint zunächst merkwürdig, denn schließlich sind Microservices „nur“ eine Architektur. Aber Architektur hat eben Auswirkungen auf die Organisation.

Die Alternative zu Microservices ist ein Deployment Monolith, in dem alle Module zusammen deployt werden. Es ist nicht möglich, ein Feature in einem Modul zu implementieren und dann nur dieses Modul zu deployen. Also muss das Deployment koordiniert werden: Alle Module müssen in einer Version vorliegen, die tatsächlich sinnvoll deployt werden kann. Außerdem muss der Deployment Monolith eine gemeinsame technische Basis haben – z.B. die Java Virtual Machine oder eine bestimmte Programmiersprache. Im Falle von Java müssen alle Bibliotheken in nur einer festgelegten Version vorhanden sein. Das erfordert, dass alle Module mit diesen Bibliotheksversionen umgesetzt sind und dass diese technische Basis auch mit allen Teams koordiniert wird (siehe Abbildung 1).

Abb. 1: Technische Koordination und Koordination der Releases über den gesamten Deployment Monolith
Abb. 1: Technische Koordination und Koordination der Releases über den gesamten Deployment Monolith

Im Gegensatz dazu können Microservices einzeln deployt werden und jeweils andere technologische Entscheidungen umsetzen (siehe Abbildung 2). Die technologischen Entscheidungen müssen nicht unbedingt dazu führen, dass jeder Microservice in einer anderen Programmiersprache implementiert ist, aber beispielsweise kann ein Team eine neue Version einer Library mit einem Bugfix ausliefern, ohne dass die anderen Microservices beeinflusst werden. Durch Microservices werden die Teams also unabhängiger. Insbesondere ich keine Koordinierung bei den Deployments mehr notwendig. Das macht beispielsweise den Release Train aus dem SAFe Framework zur Skalierung von agilen Prozessen überflüssig, weil Releases und Deployments eben nicht mehr koordiniert werden müssen, und erleichtert so das agile Vorgehen auch in großen Projekten.

Abb. 2: Technische Koordination und Koordination von Releases pro Microservice
Abb. 2: Technische Koordination und Koordination von Releases pro Microservice

Die Microservices sollten idealerweise jeweils einen Bounded Context implementieren. Das ist ein Teil der Fachlichkeit, der ein eigenes Domänenmodell hat. So kann es in einem ECommerce-System den Lieferprozess geben. Das Domänenmodell für den Bounded Context Lieferprozess kennt beispielsweise Lieferadressen oder die Größen und Gewichte der Waren. Für die Rechnungslegung muss das Domänenmodell hingegen die Preise, Steuern und die Rechnungsadressen kennen. Die Domänenmodelle bilden jeweils einige Use Cases vollständig ab. Daher kann oft eine Fachlichkeit durch Änderung von nur einem Bounded Context umgesetzt werden. Wenn der Bounded Context in einem Microservice implementiert ist, kann er unabhängig von den anderen Bounded Contexts und Microservices deployt werden. So entsteht eine große fachliche Unabhängigkeit: Ein Feature verursacht Änderungen an einem Bounded Context, der in einem Microservice implementiert ist, so dass die Änderungen mit nur einem Deployment in Produktion gebracht werden können.

Durch Microservices entsteht also ein hohes Maß an technischer Unabhängigkeit und durch Bounded Context ein hohes Maß an fachlicher Unabhängigkeit, so dass die Teams sich selbst organisieren können. Letztendlich erlaubt die Microservices-Architektur es, ein großes Projekt in mehrere kleine, unabhängige Projekte aufzuteilen, was beispielsweise den Kommunikationsaufwand und die Risiken reduziert.

Technische Vorteile

Auf technische Ebene haben Microservices ebenfalls zahlreiche Vorteile:

Herausforderungen

Natürlich bieten Microservices auch neue Herausforderungen:

Ansätze für Microservices

Wie schon erwähnt, sollten Microservices nach Bounded Contexts aufgeteilt sein. Aber einige Projekte wählen andere Ansätze. Beispielsweise teilen sie die Microservices nach Schichten auf (Abbildung 3): Auf der untersten Schicht sind Persistenz-Microservices, die alle Daten für ein Domänenobjekt wie einen Kunden enthalten. Darauf setzt dann eine Schicht mit Logik-Microservices auf, die zum Beispiel den Bestellungsprozess implementiert. Dann gibt es noch eine UI, die ebenfalls in einem Microservice umgesetzt ist oder beispielsweise eine Single Page App oder eine mobile Anwendung ist.

Abb. 3: Microservice-Aufteilung nach Schichten
Abb. 3: Microservice-Aufteilung nach Schichten

Ein Problem mit dieser Architektur ist, dass sie sehr viel Kommunikation benötigt: Von der UI müssen die Aufrufe an die Logik-Microservices gehen. Die Logik-Microservices rufen dann Persistenz-Microservices auf und zwar mehrfach, weil die Geschäftsprozesse typischerweise unterschiedliche Domänenobjekte aus unterschiedlichen Persistenz-Microservices benutzen. Die Kommunikation muss synchron sein, weil die Geschäftsprozesse direkt auf die Daten zugreifen und sie ändern müssen. Wenn einer der Persistenz-Microservices ausfällt, müssen die Logik-Microservices damit umgehen, was schwierig ist, da sie eigentlich auf die Daten angewiesen sind.

Aber auch Änderungen sind bei einem solchen Schnitt schwierig: Eine typische Änderung umfasst einen Geschäftsprozess und beeinflusst daher alle Schichten: Zusätzliche Daten müssen in den Persistenz-Microservices verwaltet werden, die Logik-Microservices müssen angepasst werden und die UI muss ebenfalls modifiziert werden, damit der Benutzer den Ablauf und die Daten verwalten kann.

Dieser Architektur-Ansatz ermöglicht also keine fachlich unabhängigen Microservices. Daher müssen sich die Teams für fachliche Änderungen eng abstimmen. So ist eine Selbst-Organisation der Teams praktisch unmöglich.

Die Microservices sind zwar technisch immer noch entkoppelt, aber das nützt nicht viel. Wenn ein Feature in Produktion gebracht werden soll, müssen mehrere Microservices koordiniert deployt werden, weil das Feature in mehreren Microservices implementiert ist. Ebenso können die Microservices immer noch isoliert ausfallen, aber wegen der vielen Abhängigkeiten ist es schwierig, die anderen Microservices gegen den Ausfall abzusichern. Auch Continuous Delivery ist nicht mehr so einfach umsetzbar: Natürlich können die Continuous-Delivery-Pipelines immer noch unabhängig sein, aber Tests für neue Features müssen sich über mehrere Microservices erstrecken. Also ist es schwieriger, die Tests auf die Pipelines aufzuteilen. Und selbst wenn die Continuous-Delivery-Pipelines unabhängig sind, nützt das nicht viel, weil ein neues Features Änderungen in mehreren Microservices und damit den Durchlauf mehrerer Continuous-Delivery-Pipelines benötigt.

Herausforderungen wie die zusätzliche Komplexität im Betrieb verringern sich bei diesem Ansatz nicht. Im Gegenteil: Die Umsetzung von Resilience wird sogar schwieriger.

Zentrale Datenbank

Ähnliches gilt für eine zentrale Datenbank. Sie enthält ein Schema, das alle Microservices nutzen. Wie schon bei der Aufteilung in Schichten begrenzt das die Unabhängigkeit der Systeme, weil so die Persistenzschicht geteilt wird. Hinzu kommt, dass die Architektur-Firewalls kompromittiert werden. Mit einer gemeinsamen Persistenz-Schicht ist es einfach möglich, die Daten anderer Microservices zu nutzen und so unabsichtlich Abhängigkeiten aufzubauen. Erfahrungsgemäß sind Abhängigkeiten auf der Datenbank-Ebene besonders eng und schwer zu entfernen. Es ist wichtig, diese Herausforderung zu kennen, denn erschreckend häufig werden Microservices mit einer zentralen Datenbank kombiniert. Das kann dann zu erheblichen Architektur-Herausforderungen führen.

Auf Ebene der Fachlichkeit ist es also wichtig, eine vernünftige Aufteilung des Systems zu definieren, weil sonst die Abhängigkeiten zwischen den fachlichen Modulen zu großen Problemen führen. Leider nehmen viele Projekte jedoch an, dass die Nutzung von Microservices alleine die Modularisierung verbessern wird. Da Microservices aber nur eine andere Art von Modulen sind, ist das natürlich nicht so.

Serverless

Technisch sind Microservices meistens mit Docker oder Kubernetes umgesetzt. Ein Microservice ist dann als ein Docker-Container implementiert. Das erlaubt Technologie-Freiheit: Was in dem Docker Container läuft, kann in jeder Programmiersprache geschrieben sein. Gleichzeitig werden Probleme wie Service Discovery oder Load Balancing gelöst [1].

Eine Alternative ist Serverless. Dieser Ansatz abstrahiert von Servern. Die kleinste Einheit bei Function as a Service (FaaS) sind Functions, die auf Events reagieren. Ein solcher Event kann ein REST-Aufruf sein oder das Hinzufügen eines Datensatzes in einer Datenbank. Diese Functions werden erst dann hochgefahren, wenn tatsächlich Requests vorliegen und skalieren abhängig von der Last. Das erlaubt ein anderes ökonomisches Modell. Eine Funktion, die gerade nicht aufgerufen wird, verursacht keine Kosten und daher müssen auch keine Kosten berechnet werden. Ebenso kann eine Funktion, die nur wenig genutzt wird, preiswert oder kostenlos angeboten werden. So können viel kleinere Microservices realisiert werden, weil die Kosten für eine Funktion vernachlässigt werden können.

Eine Implementierung von Serverless ist Amazon Lambda. Es unterstützt Java, Node.js, C# und Python. Das Serverless Application Model (SAM) erlaubt es, mehrere Funktionen zusammenzufassen und so auch größere Projekte übersichtlich umzusetzen. Für das eigene Rechenzentrum gibt es Lösungen auf der Basis von Kubernetes wie beispielsweise kubeless.

Auf der Ebene der Architektur erlaubt Serverless kleinere Microservices. Außerdem ist die Kopplung über Events vorgegeben. Auch ein synchroner REST-Call von außen wird intern in einen Event umgewandelt. Dafür sind die Technologien eingeschränkt: Es werden nicht alle Programmiersprachen unterstützt, die in einem Docker Container laufen können. Und schließlich ist FaaS nicht genug: Datenbanken und Infrastruktur für Kommunikation aber auch Monitoring ist notwendig. Diese Technologien werden von dem jeweiligen Cloud-Anbieter zur Verfügung gestellt. Dadurch entstehen natürlich zusätzliche Abhängigkeiten. Oft wird die Architektur eher zu einem Ansatz, bei dem die Serverless-Funktionen nur noch Glue Code zur Koordination verschiedener anderer Cloud-Dienste werden.

So kann Serverless als technische Weiterentwicklung von Microservices interpretiert werden: Kleinere Services, weniger Technologie-Freiheit und mehr Nutzung von vorhandenen Bausteinen.

Deployment Monolithen

Eine weitere Alternative zu Microservices sind natürlich Deployment Monolithen. Das ist zunächst eine Aussage über das Deployment: Die Anwendung wird als Ganzes deployt. Wie schon dargestellt, haben Deployment Monolithen einige technische Einschränkungen. Aber ein Deployment Monolith hat auch einen Vorteil: Das Deployment ist einfach und der Betrieb ebenfalls. Es müssen nicht eine Vielzahl von Microservices in Produktion gebracht werden und dort überwacht werden, sondern nur ein Deployment Monolith.

Leider sind wohl strukturierte Deployment Monolithen in der Praxis die Ausnahme. Der Grund dafür ist die mangelnde Trennung der Module voneinander. Es ist sehr einfach, eine Klasse aus einem anderen Modul zu nutzen und so Abhängigkeiten einzubauen, die eigentlich nicht erlaubt sind. Oft merken Entwickler das noch nicht einmal, so dass über die Zeit die Struktur des Deployment Monolithen verloren geht. Daher überrascht es, dass in der aktuellen Diskussion Deployment Monolithen als Lösung präsentiert werden, denn gerade bei der Strukturierung haben Deployment Monolithen meistens erhebliche Defizite und das sollte mittlerweile auch jedem Entwickler klar sein, denn jeder Entwickler hat wohl schon einmal an einem solchen schlecht strukturieren Deployment Monolithen gearbeitet.

Microservices haben beispielsweise REST-Schnittstellen und erzwingen daher wie schon erwähnt die Modulgrenzen. Modulgrenzen können in einem Deployment Monolithen natürlich auch erzwungen werden. Dazu können Architetur-Management-Werkzeuge wie Structure 101, jQAssistant oder Sotograph dienen. Sie warnen Entwickler, wenn sie Modulgrenzen überschreiten. Es gibt aber auch andere Möglichkeiten: So erzeugt die Build-Reihenfolge ebenfalls klare Modulgrenzen. Wenn ein Modul vor einem anderen gebaut wird, kann es das andere Modul nicht nutzen, weil es durch den späteren Build-Zeitpunkt schlicht nicht zur Verfügung steht.

Über einen Deployment Monolithen ohne solche Maßnahmen zu diskutieren, macht wenig Sinn: Sie haben eben nach einiger Zeit keine sinnvolle Strukturierung mehr und man kann nicht mehr von Architektur sprechen.

Aber selbst ein perfekt strukturierter Deployment Monolith ist eben ein Deployment Monolith. Und das begrenzt die mögliche Deployment-Geschwindigkeit. Wenn der Monolith Wochen oder Monate getestet werden muss und da Deployment einen Tag in Anspruch nimmt, dann wird es praktisch unmöglich, den Monolithen mehrfach pro Tag zu deployen, wie dies mit Microservices möglich ist. Und selbst wenn das möglich wäre: Es ist unsinnig zum Erreichen dieses Ziels eine Änderung an der Architektur von vorne herein auszuschließen.

Also können Deployment Monolithen ein sinnvoller Kompromiss sein, wenn man ein Architektur-Management einführt und mit der niedrigen Deployment-Geschwindigkeit leben kann. Selbst dann kann ein Deployment Monolith keine Entkopplung bieten wie die Microservices tun: Der Technologie Stack ist festgelegt, das Deployment muss koordiniert werden und nur das ganze System kann skaliert werden. Gerade wegen der immer noch zunehmenden Bedeutung von Continuous Delivery sind Deployment Monolithen nicht wirklich zukunftsweisend.

Quellen

  1. Eberhard Wolff: Technologien für Microservices, JavaMagazin 9.18  ↩

Fazit

Microservices sind keine Allheilmittel, mit dem sich alle Architektur-Probleme lösen lassen. Sie sind nur eine andere Art von Modulen. Wenn die Aufteilung in die Module nicht gut ist, wird sie durch Microservices nicht besser, sondern die Microservices verstärken die Probleme. Eine schlechte Modularisierung bei einem Deployment Monolithen erschwert die Entwicklung, weil mehrere Module geändert werden müssen. Sind die Module Microservices, so müssen mehrere Module koordiniert deployt werden. Das erhöht den Aufwand, den eine schlechte Modularisierung verursacht, weiter.

Die Diskussion rund um Microservices hat daher zum Glück eine wichtige Frage wieder in den Vordergrund gerückt: Was sind gute Module? Und dort bietet Domain-driven Design viele gute Ansätze. Natürlich sind Deployment Monolithen immer noch eine valide Option für die Implementierung von Systemen und sie erzeugen auch tatsächlich im Betrieb weniger Aufwände. Aber ohne zusätzliche Maßnahmen verlieren sie nach einiger Zeit ihre Struktur und außerdem begrenzen sie die Deployment Geschwindigkeit. Sie können daher kaum die Zukunft sein, denn insbesondere schnelles und häufiges Deployment wird in Zukunft noch wichtiger werden. Dennoch können Deployment Monolithen natürlich ein valider Kompromiss sein.

Serverless könnte ein sinnvoller nächster Schritt nach Microservices sein: Noch kleiner und immer noch handhabbar. Serverless schränkt zwar die Technologie-Freiheit ein, aber das kann akzeptabel sein.