Microservices

Der Begriff »Microservice-Architektur« ist in den letzten Jahren entstanden um Softwareanwendungen, die in eigenständig einsetzbare Dienste unterteilt sind, zu beschreiben. Obwohl es keine exakte Definition des Begriffs gibt, so sind doch einige Organisationsmerkmale, wie automatisierte Bereitstellung und dezentrale Datenhaltung allen gemeinsam.

Microservices – Definition

Microservices, wie wir sie verstehen, unterteilt eine einzelne Anwendung in eine Reihe von kleinen Diensten, die jeweils als eigenständiger Prozess laufen und leichtgewichtig miteinander kommunizieren, oft über eine HTTP-Resource-API.

Diese Dienste implementieren Geschäftsprozesse, die unabhängig und automatisiert einsetzbar sind. Die zentrale Verwaltung dieser Dienste wird auf ein Minimum reduziert und kann in unterschiedlichen Programmiersprachen geschrieben sein und unterschiedliche Speichertechnologien nutzen.

Abgrenzung gegen Monolithen

Die Idee der Microservices ist entstanden aus der Abgrenzung zu monolithischer Software, die verschiedene Geschäftsprozesse in einer Anwendung zusammenfasst. Genauer gesagt werden diese Monolithen üblicherweise in einer Schichtenarchitektur mit drei oder vier Schichten konzipiert:

  1. eine Benutzeroberfläche bestehend aus HTML-Seiten und Javascript, die in einem Browser dargestellt wird
  2. einer meist relationalen Datenbank bestehend aus vielen Tabellen
  3. und einer serverseitigen Anwendung die HTTP-Anfragen beantwortet, dazu fachdomänspezifische Logik ausführt und Daten aus der Datenbank aufruft und aktualisiert.

Bei einer solchen monolithischen Anwendung läuft die Logik für die Bearbeitung einer Anforderung meist in einem einzigen Prozess und für die grundlegenden Funktionen wird nur eine Programmiersprache verwendet. Die Anwendung wird dann durch Klassen, Funktionen und Namespaces aufgeteilt. Meist kann eine solche Anwendung auf dem Laptop des Entwicklers ausgeführt und getestet und eine Deployment-Pipeline verwendet werden, um sicherzustellen, dass Änderungen ordnungsgemäß getestet und in die Produktion überführt wurden. Sie können die Monolithen horizontal skalieren indem Sie viele Instanzen hinter einem Load Balancer laufen lassen.

Solche monolithischen Anwendungen wurden jahrelang von uns erfolgreich betrieben, aber sie fingen zunehmend an, uns zu frustrieren, und zwar bei den folgenden Szenarien:

  • Änderungszyklen müssen aufwändig koordiniert werden, d.h. selbst wenn nur ein kleiner Teil der Anwendung geändert werden soll, muss meist der gesamte Monolith neu gebaut und in Betrieb genommen werden.
  • Zudem wird es im Laufe der Zeit immer schwerer, eine gute modulare Struktur aufrechtzuerhalten, so dass bei Änderungen wirklich nur ein Modul betroffen ist.
  • Schließlich führt Skalierung immer dazu, dass die gesamte Anwendung repliziert werden muss, anstatt nur diejenigen Teile zu replizieren, die tatsächlich unter Last stehen und mehr Ressourcen erfordern.
Microservices

Eine monolithische Anwendung liefert alle Funktionalität in einem Prozess …

Skalierung von Microservices

… und skaliert durch Replikation des Monolithen auf mehreren Maschinen.

Um solche Frustrationen zu vermeiden, setzen wir seit vielen Jahren Microservices-Architekturen ein, die Anwendungen in einzelne Dienste unterteilt. Diese Services sind nicht nur unabhängig voneinander implementierbar und skalierbar, sondern bieten auch feste Kontextgrenzen, so dass unterschiedliche Dienste auch in der jeweils geeigneten Programmiersprache geschrieben und von verschiedenen Teams entwickelt und betreut werden können.

Monolith

Bei einer Microservices-Architektur wird jedes Funktionselement in einem separaten Service ausgeliefert …

Skalierung von Monolithen

… und skaliert durch die Verteilung dieser Dienste über Server, repliziert nach Bedarf.

Dabei erschienen uns Microservices gar nicht besonders neuartig oder innovativ – sie erinnerten uns vielmehr an eines der Designprinzipien von Unix:

Mache nur eine Sache und mache sie gut.

Dieses Designprinzip ist jedoch in monolithischen Anwendungen nicht berücksichtigt worden.

Microservices – Architektureigenschaften

Auch wenn Microservices nicht klar definiert sind, so weist ihre Architektur doch meist gemeinsame Merkmale auf.

Aufteilen der Anwendung in Servicekomponenten

Dabei haben für uns Komponenten die folgenden Eigenschaften:

  • unabhängig
  • austauschbar
  • erweiterbar

Die wesentliche Bedeutung von Microservices ist also die Aufteilung einer Anwendung in einzelne Dienste. Diese werden nicht mit anderen über In-memory-Function-Calls sprechen und unterscheiden sich damit deutlich von Serviceobjekten aus dem Domain-driven Design, die gemeinsam in nur einem Prozess ausgeführt werden.

Wesentlich für die Nutzung von Servicekomponenten und nicht als Bibliotheken ist, dass sie unabhängig voneinander eingesetzt werden können. Wenn eine Anwendung aus mehreren Bibliotheken in einem einzigen Prozess besteht, führt eine Änderung an einer einzelnen Komponente dazu, dass die gesamte Anwendung neu geordnet werden muss. Bei einer Zerlegung dieser Anwendung in mehrere Servicekomponenten jedoch sollte nur ein Service neu implementiert werden müssen. In seltenen Fällen könnte es jedoch auch hier vorkommen, dass Schnittstellen neu definiert werden müssten. Dies sollte durch die Architektur jedoch so weit wie möglich unterbunden werden.

Eine weitere Konsequenz der Verwendung von Serviceskomponenten ist eine explizite Komponentenschnittstelle. Dabei haben jedoch die meisten Sprachen leider keinen guten Mechanismus, um eine explizite Schnittstelle zu definieren. Oft ist es nur Dokumentation oder Disziplin, die verhindert, dass eine Komponentenschnittstelle geändert wird. Dies erhöht dann das Risiko einer unnötig engen Kopplung der Komponenten. Servicekomponenten vermeiden dies einfach durch explizite Remote-Call-Mechanismen.

Damit werden jedoch auch die Nachteile solcher Services offenbar:

  • Remote-Calls sind deutlich teurer als In-Process-Calls.
  • Auch sind die Remote-APIs allgemeiner gefasst und scheinen häufig schwerer bedienbar.
  • Und auch wenn die Zuordnung von Verantwortlichkeiten zwischen Komponenten geändert werden soll, so ist dies meist deutlich aufwändiger da auch Prozessgrenzen überschritten werden müssen.

Zwar werden Services häufig um einen Laufzeitprozess modularisiert, ein Service kann jedoch auch aus mehreren Prozessen bestehen, wie beispielsweise einem Prozess für die Anwendungslogik und einem für die Datenbank, die nur von diesem Service verwendet wird.

Organisation um Geschäftsprozesse

Wenn eine große Anwendung aufgeteilt werden soll, erfolgt diese häufig anhand der Technologie-Schichten, also z.B.

  • UI
  • Anwendungslogik
  • Datenbank

Wenn die einzelnen Teams jedoch genau diesen Schichten entsprechend zusammengesetzt werden, werden sie bei Änderungen meist versuchen, diese innerhalb ihres Zuständigkeitsbereichs umzusetzen. In der Folge wird sich in allen Schichten Logik wiederfinden. Dies ist nur ein Beispiel für Conways Gesetz.

Organisationen, die Systeme entwerfen, … sind auf Entwürfe festgelegt, welche die Kommunikationsstrukturen dieser Organisationen abbilden.

– Melvyn E. Conway, 1967

Funktional getrennte Teams führen zu funktional getrennter Architektur

Bei einer Microservices-Architektur wird die Anwendung nicht in unterschiedliche Schichten sondern in unterschiedliche Services unterteilt. Dabei reicht die Implementierung eines solchen Services von der persistenten Speicherung über Schnittstellen zu anderen Diensten bis hin zur Benutzeroberfläche. Infolgedessen sind die Teams funktionsübergreifend, einschließlich Expertise zur jeweiligen Datenbank, UI und Projektmanagement.

Funktionsübergreifende Teams führen zu funktionsübergreifenden Services

Produkte, nicht Projekte

Meist wird Software in einem Projekt entwickelt, bei dem die Software zu einem bestimmten Zeitpunkt ausgeliefert werden soll. Demnach wird mit der Fertigstellung die Software an ein Wartungsteam übergeben und das Projektteam aufgelöst.

Wir bevorzugen jedoch ein anderes Modell: ein Team nennt die Software über den gesamten Lebenszyklus hinweg sein eigen. Diese Verantwortung des Entwicklungsteams für die Software auch in der Produktion sehen wir auch in der DevOps-Kultur. Dadurch erhalten die Entwickler unseres Erachtens deutlich bessere Einblicke, wie sich ihre Software in der Produktion verhält und von den Anwendern angenommen wird. Durch diese Produkt-Mentatlität erhalten die Entwickler nicht nur besseren Einblick in die gewünschte Funktionalität sondern können zunehmend auch erkennen, wie der Geschäftsprozess optimiert werden könnte.

Wir betreuen unsere Anwendungen schon seit sehr langer Zeit über den gesamten Lebenszyklus hinweg, auch wenn wir sie früher häufig monolithisch, z.B. auf Basis des Web-Frameworks Zope gebaut haben, so erscheint und doch die feinere Granularität von Microservices förderlich für die Produktorientierung zu sein.

Intelligente Endpunkte und dumme Verbindungen

Bei der Erstellung von Kommunikationsstrukturen zwischen verschiedenen Diensten gibt es viele Produkte und Ansätze, die die Kommunikation deutlich erschweren können indem sie beachtliche Anforderungen an die beteiligten Komponenten stellen. Ein gutes Beispiel hierfür ist der Enterprise Service Bus (ESB), der oft anspruchsvolle Aufgaben übernehmen muss wie Message Routing, Choreographie etc.

Microservices hingegen fördern eher intelligente Endpunkte und dumme Verbindungen. Aus Microservices aufgebaute Anwendungen sind meist entkoppelt und so unzusammenhängend wie möglich. Sie besitzen ihre eigene Geschäftslogik und funktionieren wie Filter im Unix-Sinne: sie erhalten eine Anfrage, wenden ihre Logik darauf an und liefern eine Antwort zurück. Die Kommunikation erfolgt meist über einfache REST-Protokolle und nicht über komplexe Protokolle wie Service choreography, BPEL oder Orchestrierung durch zentrale Werkzeuge. Dabei sind die häufigsten Protokolle HTTP-Request-Response, ldap und Lightweight Messaging, z.B. mit RabbitMQ oder ZeroMQ. Sie übernehmen zuverlässig den asynchronen Nachrichtenaustausch ohne überhaupt nur eine Ahnung von der Geschäftslogik zu haben.

Bei einer monolithischen Anwendung sprechen die einzelnen Komponenten mit den anderen meist über einen Methoden- oder Funktionsaufruf. Und dies dürfte dann auch eines der größten Probleme sein um Änderungen bei einem solchen Monolithen vorzunehmen.

Dezentrale Organisation

Zentralisierte Organisationen neigen dazu, sich auf nur eine einzige Technologieplattform zu reduzieren. Dies kann jedoch dazu führen, dass gegebenenfalls auch ein unpassendes Werkzeug zur Lösung des Problems verwendet werden soll.

Wir wählen das passende Werkzeug für die jeweilige Aufgabe!

Wir versuchen schon lange nicht mehr alles in eine monolithischen Anwendung integrieren zu wollen. Stattdessen entwickeln wir nützliche Services, die häufig auch bei anderen Projekten wieder eingesetzt werden können.

Dezentralisierte Organisation

Wenn wir die Komponenten des Monolithen in Services aufteilen, haben wir die Wahl, wie wir sie bauen. Da verwenden wir dann z.B. ReactJS, um eine einfache Berichtseite zu erstellen und C++ für einen Real-Time-Service. Auch wählen wir die passende Datenbank für die jeweilige Datenstruktur.

Dezentrales Datenmanagement

Wenn wir über das Datenmanagement einer Anwendung nachdenken, machen wir dies häufig anhand der Kontextgrenzen (bounded context) aus dem Domain-driven Design: DDD teilt eine komplexe Fachdomäne in mehrere, möglichst klar umrissene Zusammenhänge auf und bildet daraus die Beziehungen zwischen ihnen ab. Üblicherweise lassen sich diese Kontextgrenzen sehr einfach auf Microservices abbilden wohingegen sie bei Komponenten von Monolithen sehr leicht verwischt werden können.

Neben der Teamzuordnung trennen die unterschiedlichen Kontexte auch das jeweils dahinterliegende Datenbankschemata. Microservices tendieren daher auch dazu, ihre eigenen Datenbanken zu verwalten.

Dezentralisierte Datenbanken

Die dezentrale Organisation der Teams rund um ihren Microservice führt auch zu einer verteilten Verantwortung für die Daten z.B. bei Updates. Während bei monolithischen Anwendungen meist auf Transaktionssicherheit geachtet wird, um die Konsistenz der Daten zu erhalten, so erfordert dies jedoch auch eine zeitliche Kopplung, die kaum über mehrere Services hinweg aufrechterhalten werden kann. Daher setzen die meisten Microservice-Architekturen auf eine transaktionslose Koordination zwischen den Diensten und auf eventual consistency, d.h. ein Datensatz wird irgendwann konsistent sein, sofern nur eine hinreichend lange Zeit ohne Schreibvorgänge und Fehler vorausgegangen ist.

Evolutionäres Design

Eine wesentliche Frustration bei Monolithen ist, dass sie mit der Zeit immer schwerer weiterzuentwickeln sind da eine modulare Struktur nur mühsam aufrechtzuerhalten ist und dadurch Änderungen aufwändiger und langwieriger sind. Daher erschien uns die Zerlegung in einzelne Services eine angenehme Möglichkeit zu bieten um eine Anwendung leichtgewichtig weiterentwickeln zu können.

Wir haben viele Anwendungen übernommen, die monolithisch entworfen und gebaut wurden. Meist dauert es mehrere Jahre bis wir einen solchen Monolithen vollständig ablösen können. Aber neue Features fügen wir meist als Microservices ein, die die API des Monolithen verwenden. Dieses Vorgehen ist nicht nur praktisch für nur vorübergehende Änderungen wie Kampagnen etc., sondern auch in anderen Bereichen, die sich häufig ändern. So wird die Funktionalität des ursprünglichen Monolithen immer geringer bis er dann schließlich ganz abgelöst werden kann.

Evolutionäres Design

Microservices – Nutzen und Kosten

Nutzen Kosten
Modularität
Microservices fördern die Modularität, was vor allem für große Teams hilfreich ist.
Verteilte Systeme
Verteilte Systeme sind schwieriger zu administrieren, da sie deutlich komplexer sind.
Unabhängiges Deployment
Einfache Services sind einfacher und mit geringerem Risiko bereitzustellen.
Eventual consistency
Die Aufrechterhaltung starker Konsistenzu ist in verteilten Systemen nur sehr schwierig aufrechtzuerhalten. Dies bedeutet, dass jeder Service eventuell consistency selbst managen muss.
Technologische Vielfalt
Ihr könnt die zu Eurem Service passenden Sprachen, Frameworks und Datenspeicher frei wählen.
Operationale Komplexität
Ihr benötigt qualifizierte System-Ingenieure, um die verschiedenen Dienste managen zu können.
Einfachere Skalierbarkeit
Ihr müsst nicht die gesamte Anwendung skalieren sondern nur den Teil, der zuviel Last erhält.
 
Bessere Trennungskontrolle
Mit Microservices können vertrauliche Daten getrennt werden und damit eine höhere Sicherheit erlangt werden.
 

Modularität

Microservices fördern üblicherweise die Modularität von Anwendungen, da ungenügende Modularität schnell zu sehr viel höheren Aufwänden und Risiken führt. Kleine, überschaubare Moduleinheiten lassen sich viel einfacher ändern da die Konsequenzen überschaubarer bleiben.

Ein weiterer Vorteil von Microservices sind das dezentrale Datenmanagement, bei dem jeder Service seine eigene Datenbank verwaltet und ggf. die API anderer Services durchläuft.

Verteilte Systeme

Microservices sind verteilte Systeme, wodurch die Komplexität deutlich erhöht wird.

Zunächst wirkt sich dies auf die Performance aus, da Remote-Calls deutlich langsamer sind. Wenn jeder Service jedoch viele andere Services befragen muss wird die Latenz schnell unerträglich. Dem kann zwar dadurch begegnet werden, dass die Anfragen asynchron und feingranularer gestellt werden sowie die Antworten ggf. zwischengespeichert werden.Dies verkompliziert jedoch den Programmcode erheblich.

Fehlertoleranz

Die Komplexität wird noch weiter erhöht, da Ihr Euch nicht auf eine gültige Antwort in einer bestimmten Zeit verlassen könnt. So solltet Ihr schon beim Design Eures Services darauf achten, dass dieser fehlertolerant ist. Wie das geschehen kann, ist z.B. in Principles of chaos engineering oder in Fault Tolerance in a High Volume, Distributed System. So entwickelte z.B. Netflix Chaos Monkey, der zufällig Prozesse von Instanzen und Containern beendet, um sowohl die Ausfallsicherheit als auch die Zuverlässigkeit der Gesamtanwendung zu testen. Wenn Services aber jederzeit ausfallen können, wird es umso wichtiger, den Fehler schnell zu erkennen und den Service möglichst automatisiert wieder bereitstellen zu können. Daher wird die Echtzeitüberwachung der betriebs- und geschäftsrelevanten Metriken immer wichtiger.

Dies sind nur einige der Probleme von verteilten Systemen; weitere findet Ihr in Fallacies of Distributed Computing Explained).

Unabhängiges Deployment

Ein Schlüsselprinzip von Microservices ist, dass Services Komponenten sind und daher unabhängig voneinander bereitgestellt werden können. Dies hat den Vorteil, dass Ihr bei Änderungen an einem Service nur diesen Testen und neu bereitstellen müsst. Zudem sollte selbst ein vollständiger Ausfall dieser einen Komponente nicht zum Ausfall anderer Teile des Systems führen sondern nur zu einem geringeren Funktionsumfang führen. Dieser Vorteil kommt jedoch erst richtig mit Continuous Delivery zum Tragen.

Eventual consistency

Ihr kennt sicher alle das Problem, dass Ihr ein Update durchgeführt habt und dennoch könnt Ihr die Änderungen nicht sofort sehen. Dies kann einfach damit zusammenhängen, dass gerade der eine Knoten aktualisiert wird während Eure Anfrage von einen noch nicht aktualiserten Knoten beantwortet wird. Solche Inkonsistenzen können auch zu schwerwiegenderen Problemen führen. Und auch wenn bei Monolithen leichter Transaktionssicherheit hergestellt werden kann, so sind sie keineswegs frei davon, z.B. wenn die Cache-Invalidation nicht zuverlässig ist.

Technologische Vielfalt

Da jeder Microservice eine unabhängige Einheit ist, habt Ihr auch erhebliche Freiheit bei der Auswahl Eurer Technologie. Microservices können in verschiedenen Sprachen geschrieben werden, verschiedene Bibliotheken und Datenspeicher verwenden. Auf diese Weise können Teams ein geeignetes Tool für den Job auswählen. Ihr könnt also profitieren davon, dass sich manche Sprachen und Bibliotheken besser für bestimmte Arten von Problemen eignen.

Dies ist jedoch nicht der einzige Vorteil der technischen Vielfalt von Microservices. Auch die Verwendung unterschiedlicher Versionen einer Bibliothek kann Upgrades deutlich vereinfachen. Bei einem Monolithen könnte ein Teil des Systems eine neue Funktion einer Sprache oder Bibliothek nutzen wollen, das Upgrade kann jedohch nicht verwendet werden, wenn dadurch ein anderer Teil des Monolithen kaputt geht.

Umgekehrt besteht jedoch die Gefahr, dass die technologische Vielfalt zu viel wird, sodass das Team überfordert werden kann. Die meisten mir bekannten Organisationen lassen nur eine begrenzte Anzahl von Technologien zu. Dies erleichtert dann z.B. auch die Überwachung der vielfältigen Tools da sich die Anzahl der Environments reduziert.

Operationale Komplexität

Die Möglichkeit, kleine unabhängige Module schnell bereitzustellen, ist ein großer Vortiel für die Entwicklung, stellt jedoch gleichzeitig eine zusätzliche Belastung für den Betrieb dar, da aus einem halben Dutzend Anwendungen jetzt Hunderte kleiner Mikrodienste werden können. Viele Organisationen werden die Schwierigkeit, mit einem solchen Schwarm sich schnell ändernder Tools umzugehen, als unmöglich empfinden. Tatsächlich wird dies auch nur mit Continuous Delivery möglich sein. Die Komplexität verlagert sich also von den einfacher werdenden Microservices zum gemeinsamen Betrieb dieser zunehmenden Anzahl dieser Services.

Einfachere Skalierbarkeit

Es liegt auf der Hand, dass Microservices besser den Service skalieren können, der unter erhöhter Last steht. Dies ist jedoch noch nicht alles: bei Microservices könnt Ihr auch schneller und präziser erkennen, welcher Teil unter Last steht.

Bessere Trennungskontrolle

Mit Microservices können Sie vertrauliche Daten trennen und diesen Daten mehr Sicherheit verleihen. Durch die kontrolliertere Steuerung des gesamten Datenverkehrs zwischen Microservices dürfte dies das Risiko, bei einem Einbruch umfassend Zugriff auf alle Daten zu erhalten, deutlich erschweren. Mit der zunehmenden Bedeutung von Sicherheitsproblemen ist dies eine wichtige Überlegung bei der Verwendung von Microservices.

Resümee

In diesem Artikel werden nur allgemein die Kosten und Nutzen von Microservices dargelegt. Er kann nicht die Entscheidung für ein konkretes Projekt vorwegnehmen, sondern nur dazu beitragen, dass Ihr die verschiedenen Faktoren berücksichtigt. Jeder Kosten- und Nutzenfaktor hat für verschiedene Systeme ein unterschiedliches Gewicht. Beurteilt, welche Faktoren für Euer System am wichtigsten sind und wie sie sich auf Euren speziellen Kontext auswirken. Erschwerend kommt hinzu, dass Architekturentscheidungen normalerweise erst sinnvoll beurteilt werden können, nachdem ein System ausgereift ist.

Microservices – Hype oder Zukunft?

Auch wenn wir bei vielen Anwendungen auf einer Microservices-Architektur aufgebaut haben, so sind wir doch nicht überzeugt, dass Microservices eine universelle Lösung sind.

So haben wir zwar durchaus schon Erfahrungen gemacht mit evolutionärem Design, bei dem eine einzelne Komponente ausgetauscht wurde, uns fehlt jedoch noch die Erfahrung beim Refactoring über Services hinweg: hier wird nicht nur das Verschieben von Code deutlich schwieriger als bei monolithischen Anwendungen. Auch die Koordinierung der Schnittstellenänderungen mit allen Teilnehmern, zusätzliche Schichten von Rückwärtskompatibilität und Testen verkomplizieren den Umbau.

Um die Servicegrenzen besser erkennen zu können scheint es zunächst eine gute Idee zu sein, nicht mit einer Microservices-Architektur zu beginnen sondern mit einer monolithischen Architektur, die modular aufgebaut ist (s.a. Martin Fowler: Monolith First). Wenn sich die passenden Komponenten im Lauf der Zeit bewährt haben, sollen sie dann in Microservices zerlegt haben. Diese Idee scheint uns jedoch problematisch da In-Process-Interfaces selten gut auf Service-Interfaces abbilden lassen.

Dennoch bleiben wir optimistisch, dass Microservices mit ihren Eigenschaften zu deutlich besserer Software führen, vor allem wenn es sich um komplexe Anwendungen handelt.

Produktivität und Komplexität

aus Martin Fowler: Microservice Premium