Neue Technologien wie Microservices werden meistens Top-to-Bottom eingeführt. Entwickler sind zur Anwendung gezwungen. Microservices dienen jedoch nicht dem Selbstzweck, sondern sollen konkrete Probleme lösen. Zudem wirken sich Microservices stark auf die Organisationsstruktur aus. Der Artikel basiert auf leidvollen Erfahrungen bisheriger Projekte und bietet gangbare Lösungsvorschläge.

Der Trend, neue Software auf Basis einer Microservice-Architektur zur Verfügung zu stellen, ist mittlerweile selbst in wenig innovativen IT-Landschaften angekommen. Unglücklicherweise werden neue Technologien in derartigen IT-Landschaften meistens eher top-to-bottom als bottom-up eingeführt. Das hat zur Konsequenz, dass sich viele etablierte Entwicklungsteams nun damit konfrontiert sehen, Microservices umsetzen zu müssen. Im schlimmsten Fall sogar gegen ihren Willen und gegen ihre Expertise.
In diesem Artikel werden einige grundlegende Themen diskutiert, über die vor jeder Einführung einer Microservice-Plattform diskutiert werden sollte. Er basiert auf teils leidvollen Erfahrungen bisheriger Projekte. Nach jedem weiteren Projekt wurde klarer, dass Microservices eben nicht nur die Art der Entwicklung umkrempeln, sondern auch erhebliche Anforderungen an die Organisationsstruktur stellen, wenn man eine Microservice-Architektur auch langfristig erfolgreich aus der Taufe heben will.

Microservices oder doch lieber Monolithen?

Bevor man überhaupt darüber nachdenken sollte, auf eine Microservice-Architektur zu setzen, müssen grundlegende Fragestellungen geklärt werden: Ist unsere Fachlichkeit für Microservices geeignet und ist die Organisation überhaupt fit für Microservices? Monolithen sind von Haus aus erstmal nichts Böses. Auch im Zeitalter der Microservices kann es durchaus Sinn machen, weiterhin Monolithen zu bauen. Aber tatsächlich würde es vielen Organisationen bessergehen, wenn Sie ihre Monolithen durch eine Microservice-Plattform ablösen könnten. Buchautor Martin Fowler argumentierte vor 3 Jahren in seinem Blog, nicht sofort mit einer Microservice-Architektur zu beginnen, sondern erst einen Monolithen zu bauen. Auch die Gegenrede von Autor Stefan Tilkov dazu ist sehr lesenswert. Inhaltlich scheint sich eher Tilkovs Meinung durchzusetzen.

Organisatorische und fachliche Voraussetzungen

Fangen wir also an, die organisatorischen und fachlichen Voraussetzungen für Microservices zu betrachten, bevor es technisch wird. Zunächst bedeutet ein Umbau, dass statt beispielsweise 5 nun 50 oder gar 100 Deploymenteinheiten zu verwalten sind. (Abb. 1)
Services, die vorher gruppiert deployt wurden, müssen nun einzeln verwaltet werden. Das ist primär ein Problem des Betriebes und nicht der Entwicklung. Aber die Problematik wird sehr schnell auf die Entwicklung durchschlagen: Microservices sollen schnell deployt und getestet werden können, und unabhängig voneinander lauffähig und austauschbar sein. Wenn der Prozess der Organisation es aber vorsieht, dass ein Deployment auf eine Testumgebung erst von sämtlichen Stakeholdern freigegeben werden muss, verzehnfacht sich plötzlich der Aufwand und vermutlich auch die Wartezeit. Diese Verzehnfachung schlägt sich auf alle Abschnitte der Code-to-Production-Pipeline durch: Es müssen zehnmal mehr Artefakte gebaut, getestet, nach Produktion transportiert, installiert und gemonitort werden. In einer klassischen Enterprise-IT, die nicht auf Microservices ausgelegt ist, bedeutet das eine Aufwandssteigerung, die nicht zu stemmen ist.

Microservices setzen andere Prozesse voraus

Wichtig bei der Einführung einer Microservice-Plattform ist daher die gleichzeitige Einführung von Prozessen, die das effiziente Arbeiten in der Microservice-Welt erst ermöglichen. Das geht natürlich schnell über rein organisatorische Dinge hinaus und umfasst auch die Einführung einer Toolchain, die angenehmes Arbeiten mit einer Microservice-Plattform ermöglicht. Dazu im Artikel später mehr.

DevOps ist in der Praxis oft nicht möglich

Häufig wird in diesem Kontext die Einführung von DevOps als zwingende Voraussetzung für den erfolgreichen Einsatz einer Microservice-Architektur genannt. Dahinter steckt jedoch nur die halbe Wahrheit: Natürlich ist es sehr hilfreich, wenn die Experten, die den Microservice gebaut haben, auch Verantwortung für den Betrieb übernehmen können, getreu dem Motto „You build it, you run it“. Ein solches Vorgehen ist in einigen strikt regulierten Branchen wie z.B. Banken allerdings nur bedingt umsetzbar. Dennoch darf „Wir können kein DevOps machen“ niemals eine Ausrede dafür sein, keine Microservices einzuführen. Wenn die Organisationsstruktur jedoch derart unflexibel ist, dass zwischen Entwicklern und Management keine Kommunikation stattfindet, kann eine Microservice-Plattform in der Praxis nicht funktionieren. Zumindest lesender Zugriff auf Datenschutz-unkritische Produktions-Logfiles sollte jedem halbwegs im Unternehmen etablierten Entwickler erlaubt sein, sonst findet man sich schnell in einer ineffizienten, von gegenseitigen Schuldzuweisungen geprägten Unternehmenskultur wieder.
Auch sonst ist die Kommunikationskultur für die erfolgreiche Einführung einer komplexen Architektur wie Microservices erfolgsbestimmend. Schon während der Realisierungsphase müssen die Entwickler mit dem jeweiligen Fachbereich und sonstigen Stakeholdern eng zusammenarbeiten. Etablierte Vorgehensmodelle wie SCRUM helfen da in jedem Fall weiter, wasserfallartige eher weniger.

Monolithen in Blöcke schneiden

Wenn sich die Organisation bereit fühlt Microservices einzusetzen, sollte zunächst bestimmt werden, wie die umzusetzende Fachlichkeit auf Microservices aufzuteilen ist. Im Idealfall sind die existierenden Deployment-Monolithen bereits fachlich nach Modulen geschnitten, die eine Aufteilung „Ein Microservice pro Fachlichkeit“ vorsehen. Tatsächlich ist das eine Sichtweise, die einer Microservice-Architektur entgegenkommt.
Seit einigen Jahren ist hier der Begriff „Bounded Context“ üblich. Das Konzept geht eigentlich noch weiter als Teil des „Domain Driven Designs“, doch das würde hier zu weit führen. Microservices werden so geschnitten, dass sie in sich eine Fachlichkeit abbilden können, ohne eine andere Fachlichkeit zu tangieren. Ein häufig verwendetes Beispiel ist hierfür ein Onlineshop. In einem solchen Szenario würde die Verwaltung der Kundenstammdaten auf einen einzigen Bounded Context abgebildet, der in sich alle Funktionalitäten enthält die man benötigt, um den Kundenstamm zu verwalten. Weitere Bounded Contexts sind in diesem Szenario die Verwaltung des Warenkorbs, die Auftragsabwicklung sowie die Inventarverwaltung.

Microservices eignen sich nicht immer

Das Beispiel zeigt, dass sich Abhängigkeiten zwischen Bounded Contexts nicht immer vermeiden lassen: Die Auftragsabwicklung braucht zwingend Zugriff auf die Informationen des Warenkorbs, um den Auftrag ausführen zu können. Die Kunst ist es, in der Praxis die Services derart zu schneiden, dass diese Abhängigkeiten minimiert werden. Schafft man es schon auf dem Reißbrett nicht, sinnvolle Contexts zu schneiden, sollte die Zerlegung in Microservices nochmals gut durchdacht werden. Möglicherweise ist die Anwendung aber auch zu klein, um in sinnvolle Microservices zerlegt werden zu können.

Auf die Kommunikationsrichtung kommt es an

Kleinere Deployment-Einheiten bedeuten auch, dass mehr Kommunikation zwischen diesen stattfinden muss. Das ist grundsätzlich kein Problem, solange die Richtung der Kommunikation stimmt. Die ideale Kommunikationsstruktur einer Microservice- Landschaft entspricht einem gerichteten Baum: Der Kommunikationsfluss ist immer vom Client abwärts zu den Microservices. Es gibt keine Querverbindungen zwischen den einzelnen Deploymenteinheiten. Die roten Verbindungen in (Abb. 1) schmerzen hier besonders. Ein Idealbild ohne entsprechende Querverbindungen lässt sich in der Praxis jedoch nur bei relativ simplen Landschaften erreichen.
Beim Beispiel Onlineshop wird es bereits knifflig. Beispielsweise muss ein Fullfilment-System nicht nur Zugriff auf die Lagerverwaltung, sondern auch auf die Kundenstammdaten sowie auf das Abrechnungssystem haben. Zusätzlich wird der Aufruf vermutlich nicht direkt vom Client des Kunden, sondern von einem sonstigen Umsystem ausgelöst. Um diese notwendige Querkommunikation in halbwegs geordnete Bahnen zu lenken, werden zwei Patterns genutzt, die sich gut ergänzen: API-Gateways sowie asynchrones Messaging.

Wird ein Monolith in Microservices aufgebrochen, steigt bei einem naiven Ansatz die Anzahl der Deploymenteinheiten und Kommunikationswege immens. Insbesondere Querkommunikation zwischen einzelnen Microservices ist zu vermeiden. (Abb. 1)

Microservices orchestrieren

API-Gateways sind spezielle Microservices, die von Clients aufgerufen werden, um Aufrufe weiterer Microservices zu orchestrieren. Sie sind ein Hilfsmittel, um die Aufrechterhaltung der „Baumstruktur“ in der Kommunikation zu erleichtern und erlauben es, Dienste unterhalb der API-Gateways relativ allgemein zu halten. Die Dienste können eine Schnittstelle liefern, die weitestgehend alle technisch möglichen Felder enthält und dann über den API-Gateway Service diese breite Schnittstelle für die Bedürfnisse eines bestimmten Clients ausdünnen. Beispielsweise ist ein Service für Auftragsinformationen denkbar, der zunächst alle Daten liefert, die zu einem Auftrag zur Verfügung stehen. In der API-Gateway Ebene würde es je einen API-Gateway pro Client geben, beispielsweise ein internes Admin-Tool sowie das Kunden-Frontend. Im Admin-Tool sollen all diese Daten zur Verfügung stehen, während dem Kunden jedoch nur ein Teil der Daten angezeigt werden. Diese Client-spezifische Filterung erfolgt innerhalb das API-Gateways für das Kunden-Frontend. HATEOAS-URLs, die im Microservice-Umfeld gerne eingesetzt werden, lassen sich im API-Gateway deutlich einfacher als in „tieferen“ Microservices generieren, u.a. weil nur der API-Gateway Informationen über andere Services in der Landschaft enthalten sollte, nicht jedoch die einzelnen „Low-Level“-Microservices.

Asynchrone Kommunikation zwischen Microservices

API-Gateways unterstützen bei der Orchestrierung zwischen Client- und Microservice-Landschaft, doch sie lösen nicht das Problem, dass Microservices untereinander kommunizieren müssen. Hier könnte man zwar dieselben REST-Endpunkte verwenden, die auch dem Client zur Verfügung gestellt werden.
Doch würde man dadurch eine starke Kopplung zwischen Microservices schaffen, die ja gerade vermieden werden soll. Daher wird in der Praxis häufig für die interne Kommunikation auf ein asynchrones Messaging zurückgegriffen. Microservices fordern per Message bestimmte Informationen von anderen Services an, und reagieren selbst auf eingegangene Events. So erscheint es sehr natürlich, ein von außen eingehendes Ereignis, zum Beispiel Kunde hat gezahlt, auch intern als asynchrones Ereignis weiterzuverarbeiten und darauf mit der Aktion Versende-Ware-an- Kunde zu reagieren.
Wenn wir unsere Beispielarchitektur wie in (Abb. 2) um das Konzept von API-Gateways und einen Messaging Bus erweitern, sehen die Kommunikationspfade gleich etwas freundlicher aus. Insbesondere für die Clients ergibt sich eine deutlich geringere Komplexität.

API Gateways sowie ein Messaging Bus erlauben es, den Kommunikationsfluss und die Serviceorchestrierung deutlich zu vereinfachen. (Abb.2)

Spring Boot ist das vorherrschende Framework für Microservices

In nahezu jeder halbwegs modernen Sprache lassen sich Microservices schreiben. Gleichwohl denken die meisten häufig bei Microservices an Java. Spring Boot ist hier das vorherrschende Framework. Spring Boot hat ohne Zweifel den Vorteil, schnell etwas Lauffähiges hinstellen zu können. Doch die Tücken stecken im Detail. Um sich wirklich Spring Boot Entwickler nennen zu können, genügt es nicht die Tutorials auf spring.io nachzubauen. Es sollte genügend Zeit und Mühe eingeplant werden, um die wirklichenGrundlagen von Spring Boot zu verstehen. Besonders zu nennen sind hier der Auto-Configuration bzw. Classpath- Scanning-Mechanismus sowie das Zusammenspiel des Kern-Frameworks Spring, den Erweiterungen durch Spring Boot und dem embedded Servlet-Container. Erschwerend kommt hinzu, dass die Versionierung von Spring Boot fehlleitet. Im Regelfall ist ein Wechsel der Versionen von z.B. von 1.4 auf 1.5 ein Breaking-Change, der Codeanpassungen zur Folge haben wird. Für ein Update sollte man daher genügend Zeit einplanen und unbedingt die Release Notes aufmerksam durchgehen.

Spring Boot ist nur für den Einstieg

Der Einsatz von Spring Boot oder eines vergleichbaren Frameworks ist jedoch nur der Anfang. Spring Boot hilft dem Entwickler nur einen Microservice zu bauen, jedoch nicht bei der Realisierung einer Microservice-Plattform bestehend aus hundert Microservices oder mehr. Hier benötigt es Erweiterungen wie beispielsweise Spring Cloud oder Vert.x, um die Kommunikation, Elastizität und Wartbarkeit der Plattform sicher zu stellen. Beispielsweise stellt sich die Frage, wie die Konfigurationsdateien von 100 Microservices in 5 verschiedenen Stages mit je 5 Knoten verwaltet werden sollen. Traditionelle Ansätze wie Maven-Filterung von Property Files zur Buildzeit kommen dabei schnell an ihre Grenzen was Flexibilität und Nutzbarkeit betrifft. An dieser Stelle kommt man an der Einführung eines Configservers nicht vorbei. Auch das Routing und die Versionierung ist ein wichtiger Teil einer Microservice-Plattform, der nur durch Spring Boot alleine nicht zu lösen ist. Leider sind gerade Anforderungen an Routing und z.B. gleichzeitiges Bereitstellen eines Service in verschiedenen Versionen teilweise derart unterschiedlich, dass Konzepte von der Stange nicht immer passen. Dennoch sollte man klare Argumente gegen vorhandene Lösungen wie Zuul aus dem Netflix OSS Stack sammeln, bevor man eine Eigenimplementierung auf Basis von nginx, Apache oder ähnliches in Erwägung zieht.

Build once, run anywhere

Für die Entwicklung von Microservices sind aus technischer Sicht zwei Punkte entscheidend: „Build once, run anywhere“ und „same tools, everywhere“. Ersteres bedeutet, die Software wird nur einmal gebaut und daraus resultierende Artefakt unverändert auf verschiedenen Stages (Entwicklungsumgebung, Test, Produktion) ausgeführt. Der maximale Unterschied sollten andere Startparameter sein, um z.B. umgebungsspezifische Konfigurationen oder Profile zu laden. Damit wird sichergestellt, dass genau die Software in Produktion kommt, die auch getestet wurde. Noch heute ist in vielen Enterprise-Unternehmen eine Build-Kette etabliert, in der zum Beispiel der Build für die Entwicklungsumgebung vom Entwickler erstellt wird, dann ein Systemintegrator ein weiteres Mal kompiliert um einen Build für eine Testumgebung zu erzeugen und eine dritte Person den finalen Produktions-Build erstellt. Im Worst-Case werden die unterschiedlichen Builds von unterschiedlichen Build-Verfahren erzeugt. Die große Gefahr besteht darin, dass die Builds durch leichte Unterschiede in der Buildchain für Test und Entwicklung einwandfrei funktionieren, es in Produktion jedoch zu scheinbar unerklärlichen Probleme kommt. Grundsätzlich gilt die Devise „nur was häufig verwendet wird, ist auch stabil“. Umso unglücklicher ist es, wenn die wichtigste Buildchain – die für Produktion – am seltensten genutzt wird und zudem das Artefakt für die Produktion nie mit Hilfe automatisierter Tests getestet wurde. Mit Microservices muss eine Produktivsetzung ein „Non-Event“ werden. Es kann und sollte so häufig geschehen, dass die Risiken schon aufgrund der häufigen Wiederholungen der Vorgänge sehr gering sind.

Same tools everywhere

Hierbei helfen Microservices zumindest in der Hinsicht, dass ein selbstständig lauffähiges Deployment-Artefakt transportiert wird und nicht beispielsweise getrennte Weblogic Server als Laufzeitumgebung in Test und Produktion dienen. Die beste Continuous-Delivery-Pipeline hilft jedoch nichts, wenn sie nur bis an den Rand von Testumgebungen reicht, für den Transport und das Deployen und Starten in Produktion dann aber gänzlich andere Tools verwendet werden. Auch hier gilt die Ausrede „Compliance und Prozesse“ nicht. Auch wenn ein direkter Zugriff und Transport der Artefakte aus dem Test- in das Produktionsnetz nicht möglich ist, wird es sicherlich eine Möglichkeit geben, die im Test genutzte Infrastruktur zum Starten und Stoppen auch in einer Produktionsinstanz zur Verfügung zu stellen.

Was kommt eigentlich nach Microservices?

Eine Microservice-Plattform allein ist nur der halbe Weg zur wirklich effizienten Software-Entwicklung. Erst das Konzept der „Self-Contained-Systems“ (SCS) macht die Reise komplett. Hier wird der Gedanke des Bounded Context nicht nur auf eine technische Schicht, wie z.B. Middleware, sondern auf alle involvierten Schichten ausgedehnt, d.h. Frontend, Middleware, Datenhaltung und ggf. auch Backends. Ein SCS umfasst Deployment-Einheiten oder zumindest Module für alle notwendigen Schichten. Das Frontend wird immer zusammen mit den zugehörigen Services und Daten-Layern entwickelt. (Abb. 3) zeigt exemplarisch, wie eine solche SCS-Architektur aussehen kann. Ein SCS lässt sichnur in einem vertikalen Team entwickeln, d.h. es herrscht keine Auftrennung nach Technologie, sondern rein nach Fachlichkeit. Es existiert nur noch ein Team „Produktseite“ – geschnitten analog zu den Bounded Contexts – keine Teams „Frontend für alle Fachlichkeiten“, „Middleware für alle Fachlichkeiten“ und „Datenhaltung für alle Fachlichkeiten.“ Auch die Anteile in den API-Gateways sollten vom jeweiligen SCS-Team betreut werden, sofern der API-Gateway nicht ohnehin generisch genug ist, um keine Anpassungen für die SCS zu benötigen. Damit vereinfacht sich die Abstimmungsarbeit zwischen den einzelnen Schichten enorm, die Entwicklung wird deutlich effizienter und birgt weniger Gefahr für Fehlkommunikation. Ein solches Entwicklungsvorgehen stellt jedoch eine hohe Anforderung an die verwendeten Technologien, insbesondere im Frontend-Bereich.
Dieser muss in der Lage sein, aus vielen einzelnen SCS dennoch ein für den Nutzer lückenlos zusammenhängendes Frontend zu schaffen. Hierzu ist es häufig notwendig, eine Art „Rahmen“ für das Frontend zu bauen, in welchen die einzelnen Frontend-Module der SCS nahtlos integriert werden können.

In einer SCS-Architektur umfassen die Deploymenteinheiten nicht nur klassische Middleware- Komponenten, sondern auch Datenhaltung sowie Frontend-Anteile. Die Kommunikation zwischen SCS erfolgt asynchron. (Abb. 3)

Doch bevor man sich über die Einführung über SCS Gedanken machen kann, sollte man Erfahrungen mit einer simpleren Microservice-Architektur sammeln. Microservices dienen nie einem Selbstzweck, sondern sollen konkrete Probleme und Anforderungen in der IT lösen: Flexible Skalierbarkeit, hohe Reaktionsgeschwindigkeit auf fachliche Anforderungen, geringer Aufwand für Regressionstests und Produktivsetzung sowie grundsätzlich sorgenfreies Entwickeln. In diesem Sinne: Viel Erfolg!


André GoliathAndré Goliath beschäftigt sich seit 2004 mit Full-Stack-Softwareentwicklung. Von JavaScript-Frontends über Java-Middleware bis zu Datenbanken und SAP-Backends. Seit 2012 ist er für die Senacor Technologies AG tätig. Hier berät und schult er Enterprise-Kunden und ihre Entwicklerteams in Sachen Architektur und Implementierung – und legt dabei natürlich auch immer wieder selbst Hand an den Code.


Diesen Artikel finden Sie auch in der Erstausgabe der JAVAPRO. Einfach hier kostenlos bestellen.

(Visited 696 times, 1 visits today)

Leave a Reply