Die Migration zu Microservices soll etwas an dem vorhandenen System verbessern [1][2][3][4]. Um zu verstehen, wieso Microservices besser als Architektur geeignet sein können als ein Deployment Monolith, muss man zunächst den Begriff “Microservice” definieren. Eine mögliche Definition ist, dass Microservices Module sind, die zur Organisation eines Projekts dienen – so wie Java Package oder Java Archives (JARs) auch. Ein Modul kann isoliert geändert und verstanden werden, so dass es weitgehend unabhängig von den anderen Modulen weiterentwickelt werden kann.

Die ISA Principles (Independent Systems Architecture) [5] sind eine Sammlung von Best Practices für Microservices. Der Name sagt schon, was das Ziel ist: Eine größere Unabhängigkeit, als mit klassischen Modulen möglich. Daher sind Microservices in Docker Containern laufende Prozesse. Wenn die Module in den Docker Container implementiert sind, können sie unabhängig voneinander deployt werden, jedes Modul kann andere Technologien nutzen und der Absturz eines Moduls reißt die anderen nicht mit. In einem Deployment-Monolithen laufen alle Module zusammen in einem Prozess, so dass alle Module gemeinsam deployt werden müssen. Es muss auch einen gemeinsamen Technologie-Stack muss. Ein Speicherleck bringt den gesamten Deployment-Monolithen zum Absturz. So bieten die Microservices in den Container also eine Unabhängigkeit beim Deployment, dem Technologie-Stack und beim Ausfall.

Diesen Vorteilen steht vor allem der Nachteil gegenüber, dass es bei einem Microservices-System nicht nur einen Prozess gibt, sondern eine Vielzahl von Prozessen. Das erhöht vor allem die Komplexität im Betrieb. Wenn die Vorteile diese Nachteile aufwiegen, sind Microservices die einfachste Architektur und das System sollte mit Microservices umgesetzt werden. Natürlich gilt das auch für die Migration eines Systems zu einem Microservices-System.

Migrationsstrategie: Schrittweise

Die Migration in ein Microservices-System könnte theoretisch in einem einzigen Schritt erfolgen: Bei einem Deployment wird statt dem Deployment-Monolithen eine Menge von Microservices deployt. Das erhöht das Risiko, da sich auf einem Schlag die Architektur des gesamten Systems grundlegende ändert. Die Migration hin zu Microservices kann mit der Nutzung anderer Basis-Technologien einhergehen, was das Risiko weiter erhöht. Außerdem dauert es sehr lange, bis das System vollständig migriert ist, so dass man von den Vorteilen der Microservices erst sehr spät profitiert.

Daher ist es sinnvoller, das System schrittweise zu migrieren. Es wird zunächst nur ein neuer Microservice zusammen mit dem Deployment-Monolithen in Produktion gebracht und dann schrittweise weitere Microservices. Das Risiko reduziert sich: Die Microservices werden schrittweise aus dem System herausgelöst, so dass die Änderungsschritte kleiner und dementsprechend risikoärmer sind. Außerdem profitiert das System schneller von den Vorteilen der Microservices, da schneller die ersten Teile als Microservices umgesetzt werden. Und während der Migration kann man die Prioritäten ändern und andere Teile des Systems früher oder später Microservices migrieren.

Ziele

Wie erwähnt, haben Microservices eine Vielzahl von Vorteilen. Die Migrationsstrategie muss dafür sorgen, diese Vorteile möglichst früh zu realisieren. Deswegen haben die erwarteten Vorteile der Microservices-Architektur einen erheblichen Einfluss auf die Migrationsstrategie.

Beispiel: Ziel Stabilität

In einem Projekt [6] standen meine Kollegen vor der Herausforderung, ein System zu stabilisieren. Microservices können als Lösung dafür sinnvoll sein, weil Microservices getrennt ausfallen, wie bereits besprochen. Außerdem sind Microservices verteilte Systeme, bei denen es viel mehr Server gibt und Kommunikation über das unzuverlässige Netzwerk stattfindet. Es ist also wahrscheinlich, dass irgendetwas in dem Microservices-System gerade nicht funktioniert. Damit dann nicht das gesamte System ausfällt, muss das System resilient (etwa: widerstandfähig) sein. Daher gibt es im Microservices-Bereich einige Pattern, um die Stabilität eines Systems zu erhöhen. Eine gute Sammlung stellt Michael T. Nygards Buch "Release It!” dar [7]. Auch die Patterns können die Stabilität des Systems verbessern.

Das System in dem Projekt hatte Netzwerkverbindungen zu externen Systemen, die nicht sehr zuverlässig waren. Diese Verbindungen wurden mit Timeouts versehen. Dadurch wird ein Zugriff abgebrochen, wenn er zu lange dauert. So kann erreicht werden, dass nicht etwa alle Threads blockieren, weil sie auf die Antwort externer Systeme warten. Ebenso wurden Circuit Breaker eingebaut. “Circuit Breaker” sind eigentlich Sicherungen im Stromkreislauf. Bei einem Software-System unterbricht der Circuit Breaker die Kommunikation mit einem entfernten System, sobald eine größere Anzahl Fehler aufgetreten ist. Die Aufrufe führen dann sofort zu einem Fehler, so dass auch ohne Timeout keine Ressourcen blockiert werden. Das aufgerufene System hat außerdem die Chance, sich zunächst mit weniger Last wieder zu erholen.

Im Projekt wurde dann die Stabilität durch Bulkheads erhöht. Bulkheads sind eigentlich Schotten, wie sie in Schiffen genutzt werden, um das Schiff zu unterteilen. Da die Schotten wasserdicht sind, kann bei einem Loch in der Bordwand zwar immer noch ein Teil des Schiffs voll Wasser laufen, aber nicht mehr das gesamte Schiff, so dass das Schiff weiterhin schwimmt. In einem Software-System können Bulkheads beispielsweise implementiert werden, indem Teile des Systems getrennte Thread Pools und Pools für Datenbankverbindungen haben. Wenn ein Teil des Systems alle Ressourcen aus den Pools nutzt, können andere Teile immer noch weiter funktionieren, weil sie getrennte Pools haben.

Im letzten Schritt können die Bulkheads zu Microservices umgewandelt werden (siehe Abbildung 1). Dann sind auch CPU- und Speicher-Verbrauch getrennt, weil das Betriebssystem die Prozesse gegeneinander isoliert. So führt das Ziel der Erhöhung der Stabilität zu einem Microservice-System. Andere Ziele wie unabhängige Teams waren in diesem Projekt hingegen kein Thema, so dass sie auch nicht erreicht werden müssen.

Abb. 1: Microservices-Migration mit dem Ziel Stabilität
Abb. 1: Microservices-Migration mit dem Ziel Stabilität

Ziel: Änderbarkeit

Häufig versprechen sich Teams von der Migration in ein Microservices-System aber eine bessere Änderbarkeit des Systems, weil viele Systeme schlecht strukturiert sind und damit auch schwer änderbar. Der Code stellt dann keinen Wert, sondern eher ein Hindernis dar.

Microservices bieten die Möglichkeit, zumindest technisch von vorne anzufangen. Jeder Microservice kann mit einer anderen Technologie umgesetzt werden und natürlich kann die Technologie auch eine ganz andere sein, als die bei dem vorhandene benutzte.

In einem solchen Szenario ist es kaum sinnvoll, die aktuelle Architektur zu untersuchen, um so eine sinnvolle Aufteilung in Microservices zu ermitteln. Die Struktur des Codes stellt diese Architektur dar und gerade diese Struktur soll ja verbessert werden. Man kann den technologischen mit einem architekturellen Neustart kombinieren. Das vorhandene System dient dann als Black-Box-Vorlage: Es wird von außen bezüglich der implementierten Funktionalitäten analysiert, aber die internen Strukturen werden nicht weiter betrachtet. Schließlich sollen diese Strukturen ja gerade verbessert werden.

Natürlich ist es denkbar, dass die Architektur des vorhandenen Systems gar nicht schlecht ist. Wenn zudem die Technologien auch noch tragfähig sind, ist gegebenenfalls eine Verbesserung des Deployment-Monolithen eine sinnvolle Alternative. Wenn das System doch analysierbar ist, kann natürlich die Struktur beibehalten werden. Für jeden dieser Fälle muss man die richtige Strategie wählen.

Microservices – einfach änderbar?

Wie schon erwähnt, sind Microservices nur eine andere Art von Modulen. Ein Microservices-System kann eigentlich nicht einfacher änderbar sein als ein Deployment-Monolith. Wenn die Strukturierung in Module nicht stimmt, wird das System schwer änderbar sein – unabhängig davon, ob die Module als Microservices implementiert sind oder nicht.

Durch die Microservices-Diskussion wird aber der Aufteilung von Systemen in Module wieder mehr Aufmerksamkeit geschenkt. Als Werkzeug für die Aufteilung hat sich Domain-driven Design [8] etabliert. Als grobgranulare Modularisierung kommen Bounded Context in Betracht. Jeder Bounded Context hat sein eigenes Domänenmodell. So kann es beispielsweise ein Bounded Context mit einem Domänenmodell für das Erzeugen von Rechnungen einschließlich der Berechnung der Steuern und der Bezahlung geben und einen anderen Bounded Context für das Versenden der Waren einschließlich Reklamationen und Tracking. Die Bounded Context sind lose gekoppelt: Änderungen im Versand und bei den Rechnungen betreffen jeweils nur einen Bounded Context. Sicher haben die Bounded Context Abhängigkeiten, aber in vielen Fällen schlagen Änderungen nicht auf andere Bounded Context durch.

Migration: Blue Print

Ein typisches Vorgehen für die Migration hin zu Microservices kann man als “Blue-Print-Ansatz” bezeichnen (siehe Abbildung 2).

Abb. 2: Blue-Print-Migration nach Bounded Context mit asynchroner und UI
/ API-Integration
Abb. 2: Blue-Print-Migration nach Bounded Context mit asynchroner und UI / API-Integration

Ziel einer Migration zu einem Microservices-System sollte es sein, das System in Bounded Contexts aufzuteilen und jeden dieser Bounded Contexts als Microservice zu implementieren. Um die Vorteile der Bounded Contexts von Anfang an zu nutzen, sollte idealerweise zunächst eine Bounded Context vollständig migriert werden. Basis kann dazu eine Analyse sein, mit der das aktuelle System in Bounded Contexts aufgeteilt wird. Dazu kann es ausreichen, das System von außen zu untersuchen.

Dann kann ein Bounded Context für die Migration in einem Microservice ausgewählt werden. Dabei spielt das Risiko eine Rolle. Ein Bounded Context, der nicht so wichtig ist, kann ohne größeres Risiko umgestellt werden. Das erleichtert es, neue Technologien oder den Microservice-Ansatz auszuprobieren. Ein anderer Faktor ist der Nutzen der Migration eines Bounded Contexts. Wenn ein Bounded Context in absehbarer Zukunft häufig geändert werden wird, dann kann eine Migration in einem Microservice sich schnell rechnen. Schließlich soll die Migration ja die Entwicklung vereinfachen. Das ist besonders nützlich, wenn viel an dem Bounded Context entwickelt wird.

Der neue Microservice implementiert dann einen Bounded Context und hat daher ein eigenes Domänenmodell. Einige Daten aus dem Domänenmodell müssen dauerhaft z.B. in einer Datenbank gespeichert werden. So ergibt sich, dass jeder Microservice ein eigenes Schema in der Datenbank hat. Natürlich kann der Microservice auf Daten aus dem vorhandenen System oder aus anderen Microservices angewiesen sein. In dem Fall muss der Microservice diese Daten selbst ebenfalls speichern.

Durch die Aufteilung in Bounded Contexts kann eine neue Anforderung oft in nur einem Microservice implementiert werden. Wenn der Microservice auch die UI enthält, ist es selbst dann noch möglich, eine Änderung in einem Microservice zu isolieren, wenn die Änderung nicht nur das Domänenmodell, sondern auch die Benutzeroberfläche umfasst.

Neuer Bounded Context statt Migration

Statt einer Migration eines vorhandenen Bounded Context kann auch ein vollständig neuer Bounded Context mit neuen Features implementiert werden. Das hat zahlreiche Vorteile:

Es gibt also drei Möglichkeiten, einen Bounded Context für einen neuen Microservice zu identifizieren:

Integration

Der Microservice muss nun mit dem vorhandenen System integriert werden. Dazu bietet sich eine asynchrone Integration an. Bei asynchroner Kommunikation wartet der Aufrufer nicht darauf, dass die Nachricht bearbeitet wird. Diese Art der Kommunikation ist gut dazu geeignet, Events zu verteilen. Events repräsentieren eine Information über etwas, das passiert ist. Es ist daher nicht notwendig, auf eine Bearbeitung eines Events zu warten. Gerade für die Kommunikation zwischen Bounded Contexts sind Events gut geeignet.

Eine synchrone Kommunikation lässt sich in einigen Fällen nicht vermeiden. Sie führt aber zu einer engen Kopplung. Außerdem muss bei einer synchronen Kommunikation damit umgegangen werden, dass der Kommunikationspartner gerade ausgefallen ist. Das ist schwierig, da synchrone Kommunikation ja gerade bedeutet, dass der Aufrufer auf das Ergebnis des Aufrufs wartet – vermutlich, weil er sonst nicht weiterarbeiten kann. Also ist es schwierig, ohne dieses Ergebnis einfach weiterzumachen.

Auch auf Ebene der Web-Schnittstelle kann eine Integration sinnvoll sein. Wenn es beispielsweise einen Microservice für den Bestellvorgang gibt, kann dieser Microservice über Links aus dem vorhandenen System aktiviert werden. Wenn das vorhandenen System eine API anbietet, können bestimmte Requests an den neuen Microservice geschickt werden.

Bei der Integration muss auch die Authentifizierung betrachtet werden: Es ist einem Benutzer kaum zuzumuten, sich bei dem vorhandenen System und dann zusätzlich noch bei den Microservices zu authentifizieren.

…und so weiter

Nach der Migration des ersten Bounded Context kann der Nächste analog migriert werden. Bei der Auswahl spielen das Risiko und der erwartete Nutzen wieder eine sehr wichtige Rolle.

Es kann vorkommen, dass die Migration sehr lange dauert oder gar kein Ende hat. Das scheint auf den ersten Blick ein Problem zu sein. Aber in Wirklichkeit bedeutet das nur, dass das Risiko zu hoch ist oder der Nutzen zu gering. Man kann also einfach mit der Migration aufhören, wenn sich kein Nutzen mehr einstellt. Das verbessert die Wirtschaftlichkeit der Migration.

Organisation

Durch die Bounded Contexts bilden die Microservices unabhängige fachliche Einheiten ab. Gleichzeitig sind sie technische unabhängig: In jedem Microservice kann ein anderer Technologie-Stack genutzt werden. Das ermöglicht es, die Arbeit an einem Microservice weitgehend unabhängig von den anderen Microservices zu gestalten. So kann ein wichtiges Ziel von Microservices erreicht werden: Die Skalierung des Entwicklungsprozesses durch unabhängige Teams. Aber dazu muss die Verantwortung für Entscheidungen auch tatsächlich an die Teams delegiert werden. Das erfordert eine Änderung an der Organisation und an der Kultur. Aber oft wird zwar eine Microservices-Architektur eingeführt, während die Organisation und Kultur unangetastet bleiben. Dann können Microservices natürlich ihre Wirkung nicht voll entfalten.

Andere Strategien

Gerade bei der Migration hin zu Microservices sind generelle Regeln schwierig, da jedes Projekt anders ist und auch die Ziele für die Migration grundverschieden sein können. Der Blue-Print-Ansatz muss daher auf die jeweilige Situation angepasst werden. Manchmal ist der Blue-Print-Ansatz sogar ungeeignet, um die Ziele der Migration zu erreichen. Daher bieten sich alternative Migrationsstrategien an:

Abb. 3: Microservices-Migration nur im Portal oder nur im Backend
Abb. 3: Microservices-Migration nur im Portal oder nur im Backend

Links & Literatur

  1. Eberhard Wolff: Microservices: Grundlagen flexibler Softwarearchitekturen, 2. Auflage, dpunkt, 2018, ISBN 978–3864905551  ↩

  2. Eberhard Wolff: Das Microservices–Praxisbuch: Grundlagen, Konzepte und Rezepte, dpunkt, 2018, ISBN 978–3864905261  ↩

  3. Eberhard Wolff: Microservices Rezepte – Technologien im Überblick, kostenlos unter https://microservices-praxisbuch.de/rezepte.html  ↩

  4. Eberhard Wolff: Microservices – Ein Überblick, kostenlos unter https://microservices-buch.de/ueberblick.html  ↩

  5. https://isa-principles.org/  ↩

  6. https://www.innoq.com/de/talks/2015/11/javaday-kyiv-modernization-legacy-systems-microservices-hystrix/  ↩

  7. Michael T. Nygard: Release It!: Design and Deploy Production–Ready Software, Pragmatic Bookshelf, 2nd Edition, 2017, ISBN 978 1 68050 239 8  ↩

  8. Eric Evans: Domain–driven Design Referenz, kostenlos unter http://ddd-referenz.de/  ↩

Fazit

Die Migration zu einem Microservice-System hängt ganz entscheidend von den Zielen der Migration ab. Für das Ziel einer einfach weiterentwickelbaren Architektur bietet sich der Blue-Print-Ansatz an, der die Microservices nach Bounded Context aus dem vorhandenen System herauslöst. Bounded Contexts führen zu einer hohen fachlichen Unabhängigkeit. Anschließend müssen das vorhandene System und die Microservices integriert werden.

Neben den Zielen der Migration ist auch die Organisation ein wichtiger Einflussfaktor für die Migration: Eine Skalierung der Entwicklung und die dafür notwendigen unabhängige Teams können nur durch eine Reorganisation erreicht werden. Wenn eine Reorganisation unmöglich ist, können die Teams nicht anders aufgeteilt werden. Dann muss die Aufteilung in Microservices so gewählt werden, dass dennoch ein Microservice in die Verantwortung eines Teams gehört.

Die Migration sollte schrittweise Microservices erfolgen und dabei nach Risiko und erwarteten Vorteilen vorgehen. Das stellt sicher, dass das Vorgehen wirtschaftlich sinnvoll ist. Sollte eine weitere Migration keinen Sinn ergeben, kann man einfach aufhören und die restlichen Bestandteile nicht migrieren.