Blog

Vom Monolithen zu Microservices – Eine Architektur Strategie

Die meisten Menschen außerhalb der IT bekommen meistens nicht mit, wie schwierig es ist, komplexe Enterprise-Systeme zu verwalten. Es ist ein feiner Balanceakt der auf dem Verständnis beruht, wie sich eine Veränderung auf das gesamte System auswirken wird.

Neue Entwickler verbringen Monate damit, die Codebasis des Systems zu studieren, bevor sie anfangen können daran zu arbeiten. Sogar die kenntnisreichsten Entwicklungsteams zögern Änderungen vorzunehmen oder neuen Code hinzuzufügen, weil es den Betrieb in einer unvorhergesehenen Weise stören könnte. Das hat zu Folge, dass selbst banalste Änderungen diskutiert und hinausgezögert werden.

Wenn Dinge schief gehen, beschuldigen sich Administration, Entwicklung und QA gegenseitig. Das Projektmanagement macht das fehlende Budget verantwortlich usw. Die Folge ist, dass  Unternehmen ihr Vertrauen in die IT verlieren und anfangen nach Outsourcer zu suchen, um das interne Team zu ersetzen.

Wenn Sie nicht gerade von einem Sabbatical wieder gekommen sind haben Sie sicherlich gehört wie Microservices dieses Szenario auf den Kopf stellen können, so dass eine neue, agilere Welt entsteht in dem Entwickler und Operations-Teams Hand in Hand arbeiten, um kleine, lose gekoppelte Software Bundles zu liefern.  Anstelle eines einzigen monolithischen Systems wird die Funktionalität von einem kleineren Satz von Diensten durchgeführt, die ihre Operationen koordinieren.

Wie macht man das? In diesem Post betrachten wir einen möglichen Ansatz. Während ein „One-Size-Fits-All-Ansatz“ zur Akzeptanz von Microservices nicht existieren kann, ist es hilfreich, die Grundprinzipien zu untersuchen, die erfolgreiche Migrationen gemein haben.

Microservices Adaptieren

Ein gemeinsamer Ansatz für Teams, die Microservices adaptieren wollen, besteht darin, die vorhandene Funktionalität im monolithischen System zu identifizieren, die sowohl unkritisch als auch ziemlich lose mit dem Rest der Anwendung gekoppelt ist. Zum Beispiel sind in einem E-Commerce-System Produkte und Angebote oft ideale Kandidaten für ein Microservices-Proof-of-Concept. Alternativ können anspruchsvollere Teams einfach die Vereinbarung treffen, dass alle neuen Funktionalitäten als Microservice entwickelt werden müssen.

In jedem dieser Szenarien besteht die zentrale Herausforderung darin, die Integration zwischen dem bestehenden System und den neuen Microservices zu entwerfen und zu entwickeln. Wenn ein Teil des Systems mit Microservices neu gestaltet wird, ist eine gängige Praxis Glue Code einzuführen, um eine Schnittstelle zu den neuen Diensten zu haben. Ein API-Gateway kann dazu beitragen, viele individuelle Service-Anrufe zu einem grobkörnigen Service zu kombinieren und damit die Kosten für die Integration mit dem monolithischen System zu reduzieren.

Um den Übergang zu unterstützen besteht die Hauptidee darin, die Funktionalität im System mit diskreten Microservices langsam zu ersetzen und gleichzeitig die Änderungen zu minimieren, die dem System selbst hinzugefügt werden müssen. Dies ist wichtig, um die Kosten für die Aufrechterhaltung des Systems zu reduzieren und die Auswirkungen der Migration zu minimieren.

Microservices Architektur Muster

Es gibt eine Reihe von architektonischen Mustern, die genutzt werden können, um eine solide Microservices-Implementierungsstrategie aufzubauen.

In ihrem Buch „The Art of Scalability“ erläutern Martin Abbott und Michael Fisher das Konzept des „Skalierungs-Würfels“, in dem verschiedene Möglichkeiten der Skalierung von Microservices veranschaulicht werden (Abbildung 1).

Das Microservices Muster selbst befindet sich auf der Y-Achse des Würfels, wobei eine funktionelle Zersetzung zur Skalierung des Systems verwendet wird. Jeder Dienst kann dann durch Klonen (X-Achse) oder Sharding (Z-Achse) weiter skaliert werden.

Alistair Cockburn stellte das „Ports and Adapters“ Muster vor, das auch hexagonale Architektur genannt wird. Obwohl dieses Muster im Zusammenhang mit dem Aufbau von Anwendungen, die isoliert getestet werden können entstanden ist wird es auch zunehmend für den Aufbau von wiederverwendbaren Microservices verwendet. Eine hexagonale Architektur ist die Implementierung eines Musters, das als Bounded Context bezeichnet wird, wobei die Funktionalitäten, die sich auf eine bestimmte Business Domain beziehen, von irgendwelchen äußeren Änderungen oder Effekten isoliert sind.

Beispiele, in denen diese Grundsätze von Unternehmen in die Praxis umgesetzt wurden:

  • Click Travel: hat sein Cheddar-Framework als Open Source freigegeben. Es ist eine einfach zu nutzende Projektvorlage für Java-Entwickler, die Anwendungen für Amazon Web Services erstellen.
  • SoundCloud: nach einem gescheiterten Versuch eines Big-Bang-Refactorings ihrer Anwendung, basierte ihre Microservices Migration auf die Verwendung des Bounded Context Architektur Musters, um zusammenhängende Feature-Sets zu identifizieren, die nicht mit dem Rest der Domain gekoppelt waren.

Eine Herausforderung von Teams, die anfangen mit Microservices zu arbeiten, sind verteilte Transaktionen, die mehrere unabhängige Dienste umfassen. In einem monolithischen System ist dies einfach, da Zustandsänderungen typischerweise auf einem gemeinsamen Datenmodell basieren, das von allen Teilen der Anwendung gemeinsam genutzt wird. Dies ist jedoch bei Microservices nicht der Fall.

Mit jedem Microservice, der seinen eigenen Zustand und Daten verwaltet, wird eine architektonische und operative Komplexität bei der Handhabung verteilter Transaktionen eingeführt. Gute Designpraktiken, wie z. B. Domain-Driven Design, helfen, diese Komplexität zu verringern, indem sie den gemeinsamen Zustand inhärent einschränken.

Eventorientierte Muster wie Event Sourcing oder „Command Query Responsibility Segregation“ (CQRS) können Teams helfen, Datenkonsistenz in einer verteilten Microservices-Umgebung zu gewährleisten. Mit Event-Sourcing und CQRS können die Zustandsänderungen, die zur Unterstützung von verteilten Transaktionen erforderlich sind, als Ereignisse (Event Sourcing) oder Befehle (CQRS) propagiert werden. Jeder Microservice, der an einer bestimmten Transaktion teilnimmt, kann dann das entsprechende Event abonnieren.

Dieses Muster kann erweitert werden, um Kompensationsoperationen durch den Mikroservice zu unterstützen, wenn es um eventuelle Konsistenz geht. Chris Richardson stellte eine Implementierung dieses Musters in seinem Vortrag bei hack.summit () 2014 vor und teilte Beispielcode über GitHub. Es lohnt sich auch, Fred George’s Vorstellung von „Streams and Rapids“ zu studieren, die asynchrone Dienste und einen Hochgeschwindigkeits-Messaging-Bus nutzen, um die Microservices in einer Anwendung zu konnektieren.

Solche Architekturen wie diese sind vielversprechend. Es ist wichtig daran zu denken, dass während des Übergangs von einem monolithischen System zu einer Sammlung von Microservices beide Systeme parallel existieren. Um die Entwicklungs- und Betriebskosten der Migration zu reduzieren, müssen die architektonischen und Integrationsmuster der Microservices der Architektur des Systems entsprechen.

Architektonische und Implementierungsüberlegungen

Domänenmodellierung

Domänenmodellierung steht im Mittelpunkt der Gestaltung kohärenter und lose gekoppelter Microservices. Das Ziel ist es, sicherzustellen, dass jede der Microservices Ihrer Anwendung ausreichend von den Laufzeit-Nebenbedingungen isoliert und von Änderungen in der Implementierung der anderen Microservices im System isoliert ist.

Die Isolierung von Microservices sorgt auch für deren Wiederverwendbarkeit. Betrachten Sie z.B. einen Angebots-Service, der aus einem monolithischen E-Commerce-System extrahiert werden kann. Dieser Dienst könnte von verschiedenen Consumer (Clients) mit mobilen Web-, iOS- oder Android-Apps verwendet werden. Damit dies vorhersehbar funktioniert, muss die Domäne von „Angebot“, einschließlich ihrer Entitäten und Logik, von anderen Domänen im System isoliert werden, wie „Produkte“, „Kunden“, „Aufträge“ usw. Das bedeutet Der Angebots-Service darf nicht mit domänenübergreifenden Logiken oder Entitäten überlagert werden.

Die richtige Domänenmodellierung hilft auch, die Modellierung des Systems entlang technologischer oder organisatorischer Grenzen zu vermeiden, was zu Datendiensten, Geschäftslogik und Präsentationslogik führt, die jeweils als separate Dienste implementiert werden.

Sam Newman diskutiert diese Prinzipien in seinem Buch „Building Microservices“. Vaughn Vernon konzentriert sich auf diesen Bereich noch tiefer in „Implementing Domain-Driven Design“.

Service Größe

Service Größe ist ein weit verbreitetes und verwirrendes Thema in der Microservices-Community. Das übergeordnete Ziel bei der Bestimmung der richtigen Größe für einen Microservice ist es die Entstehung eines Monolithenzu vermeiden.

Das „Single Responsibility Principle“ ist eine treibende Kraft bei der Betrachtung der richtigen Servicegröße in einem Microservice-System. Einige Praktizierende befürworten so eine kleine Dienstgröße wie möglich für den unabhängigen Betrieb und das Testen. Microservices sollten eine kleine Codebasis aufweisen weil sie dann einfacher zu pflegen und zu aktualisieren sind.

Architekten müssen besonders bei der Erstellung großer Domänen, wie „Produkte“ in einem E-Commerce-System, vorsichtig sein, da es sich hierbei um potenzielle monolithische Designs handelt, die zu großer Variation neigen; z.B. könnte es verschiedene Arten von Produkten geben. Für jede Art von Produkt könnte es eine unterschiedliche Geschäftslogik geben. Das Kapseln all dieser Variationen könnte überwältigend werden. Ein Weg könnte sein schärfere Grenzen in der Produkt Domäne zu ziehen und somit mehrere Microservices daraus abzuleiten.

Eine weitere Überlegung ist die Idee der Austauschbarkeit. Wenn die Zeit, die es braucht, um einen bestimmten Microservice durch eine neue Implementierung oder Technologie zu ersetzen, zu lang ist (relativ zur Zykluszeit des Projekts), dann ist es definitiv ein Service, der eine weitere Nachbearbeitung seiner Größe benötigt.

Testen

Schauen wir uns einige operative Aspekte an, dass das monolithische System schrittweise in ein microservicebasiertes System überführt. Testability ist ein häufiges Problem: Im Zuge der Entwicklung der Microservices müssen die Teams die Integrationstests der Services mit dem monolithischen System durchführen. Die Idee ist natürlich, dafür zu sorgen, dass die Geschäftsvorgänge, die das bereits bestehende monolithische System und die neuen Microservices überspannen, weiterhin funktionieren

Eine Option hier ist, dass das System einige consumer basierte „Verträge“ oder Schnittstellen zur Verfügung stellt, die in Testfälle für die neuen Microservices übersetzt werden können. Dieser Ansatz der automatisierten Tests hilft sicherzustellen, dass der Microservice immer der Schnittstellenspezifikation genügt. Die Entwickler des Systems würden eine Spezifikation liefern, die Musteranforderungen und erwartete Microservice-Antworten enthält. Diese Spezifikation wird dann verwendet, um relevante Mocks zu erstellen und als Grundlage für eine automatisierte Test-Suite zu nutzen.

„Pact“, eine Consumer-getriebene Vertragstestbibliothek, ist eine gute Referenz für diesen Ansatz. Erstellen einer wiederverwendbaren Testumgebung, die eine Testkopie des gesamten Systems bereitstellen kann.

Dies eliminiert potenzielle Roadblocks für diese Teams und verbessert die Feedbackschleife für das Projekt als Ganzes. Ein Weg, dies zu erreichen, besteht darin, das gesamte System in Form von Docker-Containern zu verpacken, die durch ein Automatisierungswerkzeug wie Docker Compose orchestriert werden. Dies ermöglicht den schnellen Einsatz einer Testinfrastruktur des Systems und gibt dem Team die Möglichkeit, Integrationstests lokal durchzuführen.

Service Discovery

Ein Service muss bei der Erfüllung einer Geschäftsfunktion über andere Dienste im System Bescheid wissen. Ein Service Discovery System ermöglicht dies, wobei jeder Dienst auf eine externe Registrierung verweist, die alle Endpunkte der anderen Dienste hält. Dies kann durch Umgebungsvariablen im Umgang mit einer kleinen Anzahl von Diensten umgesetzt werden; Etcd, Consul und Apache Zookeeper sind Beispiele für anspruchsvollere Systeme, die üblicherweise für Service Discovery verwendet werden.

Deployment

Jeder Microservice sollte für sich deployable sein, entweder auf einem Laufzeitcontainer oder durch Einbetten eines Containers in sich selbst. Zum Beispiel könnte ein JVM-basierter Microservice einen Tomcat-Container in sich einbetten, wodurch der Bedarf an einem eigenständigen Web-Applikationsserver entfällt. Zu jedem Zeitpunkt könnte es eine Anzahl von Microservices desselben Typs geben (dh X-Achsen-Skalierung nach dem Skalenwürfel), um eine zuverlässigere Bearbeitung von Anfragen zu ermöglichen. Die meisten Implementierungen beinhalten auch einen Software-Load-Balancer, der auch als Service-Registry wie Netflix Eureka fungieren kann. Diese Implementierung ermöglicht auch Failover und transparente Abwägung von Anfragen.

Build & Release Pipelines

Zusätzliche Überlegungen bei der Implementierung von Microservices sind die Continuous Integration und Continous Deployment Pipeline. Dabei ist die Idee für jeden Microservices eine separate Pipeline bereitzustellen. Dies reduziert die Kosten für den Bau und die Freigabe der Anwendung als Ganzes.

Release-Praktiken müssen auch das Konzept der Rolling Upgrades oder Blue-Green-Deployments enthalten. Dies bedeutet, dass zu jedem Zeitpunkt in einem neuen Build- und Release-Zyklus gleichzeitige Versionen des gleichen Microservice in der Produktionsumgebung vorhanden sein können. Ein Prozentsatz des aktiven Benutzertraffics kann auf die neue Microservice-Version geleitet werden, um seinen Betrieb zu testen, bevor langsam die alte Version ausläuft. Dies hilft sicherzustellen, dass eine fehlgeschlagene Änderung in einem Microservice das System nicht lähmt. Im Falle eines Ausfalls kann die aktive Last zurück zur alten Version des gleichen Dienstes geleitet werden.

Feature-Flags

Ein weiteres gemeinsames Muster ist es, Feature Flags zuzulassen. Ein Feature Flag, ein Konfigurationsparameter, kann dem System hinzugefügt werden, um das Ein- und Ausschalten eines Features zu ermöglichen. Die Implementierung dieses Musters in dem System würde es uns ermöglichen, die Verwendung des relevanten Microservice für das Merkmal auszulösen, wenn das Merkmal eingeschaltet ist. Dies ermöglicht ein einfaches A / B-Testing von Features, die vom monolithischen System zu Mikrodiensten migriert wurden. Wenn die vorhandene Version eines Merkmals und der neue Microservice, der das Feature repliziert, in der Produktionsumgebung koexistieren kann, kann eine Traffic-Routing-Implementierung zusammen mit dem Feature-Flag den Delivery-Teams helfen, das Endsystem schneller zu erstellen.

Entwickler-Produktivität während der Microservices-Adaption

Monolithische Architekturen sind attraktiv, da sie eine schnelle Erstellung von neuen Business Features bei einem engen Zeitplan ermöglichen – WENN das Gesamtsystem noch klein ist. Allerdings wird dies ein Entwicklungs- und Operations Albtraum, wenn das System wächst.

Wenn die Arbeit mit dem Monolithen immer so schmerzhaft wäre würden Sie wahrscheinlich von der Verwendung eines solchen System absehen. Vielmehr werden Systeme zu Monolithen, weil es anfangs einfach ist schnell Features hinzuzufügen.

Technische Schulden werden aufgebaut und irgendwann müssen sie zurückgeszahlt werden. Es spricht nichts gegen das Hinzufügen von Features auch bei engen Zeitplänen, allerdings muss das System architektonisch dafür gemacht sein.

Geben Sie den Entwicklern die Möglichkeit, einen „Microservices First“ -Ansatz zu wählen, wenn Sie ein neues Feature oder System aufbauen. Dies erfordert starke Disziplin rund um Architektur und Automatisierung, was wiederum dazu beiträgt, eine Umgebung zu schaffen, die es Teams ermöglicht, schnell und sauber Microservices zu bauen.

Ein Ansatz für den Aufbau dieser Entwicklerinfrastruktur besteht darin, ein Standard-Boilerplate Projekt Template zu schaffen, das die Grundprinzipien des Microservice-Designs einschließt, einschließlich Projektstruktur, Testautomatisierung, Integration mit Instrumentierungs- und Überwachungsinfrastrukturen, Muster wie Circuit breaker und Timeouts, API-Frameworks und Documentation Hooks, etc.

Projektvorlagen ermöglichen es den Teams, sich weniger auf Standardstrukturen und Glue Code zu konzentrieren, und mehr auf den Aufbau von Business-Funktionalität in einer verteilten Microservice Umgebung. Projekte wie Dropwizard, Spring Boot und Netflix Karyon sind interessante Ansätze, dies zu lösen. Die richtige Wahl zwischen diesen Frameworks ist abhängig von der Architektur und den Entwickler-Qualifikationen.

Überwachung und Betrieb

Koexistenz von monolithischen Systemen und Microservices erfordert eine umfassende Überwachung von Performance, Systemen und Ressourcen. Dies ist stärker ausgeprägt, wenn ein bestimmtes Feature des Systems durch einen Microservice repliziert wird. Das Sammeln von Statistiken für Performance und Last ermöglicht auch einen Vergleich zwischen der monolithischen Implementierung und der microservice basierten Implementierung. Dies ermöglicht eine bessere Sicht auf die „Wins“, die die neue Umsetzung bringt und erhöht das Vertrauen in die Weiterentwicklung der Migration.

Organisatorische Überlegungen

Die anspruchsvollsten Aspekte der Migration von monolithischen Systemen zu Microservices sind die notwendigen organisatorischen Veränderungen, wie z.B. der Aufbau von Service Teams, die über alle Aspekte ihrer Microservices selbst bestimmen können. Dies erfordert die Erstellung von multidisziplinären Einheiten, die unter anderem Entwickler, Tester und Operationsmitarbeiter umfassen.

Fazit

Die meisten der in diesem Artikel aufgezeigten Ideen werden bereits praktiziert oder haben schon Ergebnisse in Organisationen aller Größen geliefert. Allerdings sind diese nicht in Stein gemeißelt. Daher ist es wichtig, ein Auge auf die Entwicklung von Architektur Mustern und deren Adaptionen zu haben. Da sich mehr Organisationen von monolithischen Systemen zu Microservices bewegen, haben wir noch sehr viel auf dieser Reise zu lernen.

Masiar IghaniVom Monolithen zu Microservices – Eine Architektur Strategie