Veit Schiele Communications

Veit Schiele Communications
StartseiteBlogMicroservices

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.
erstellt von Veit Schielezuletzt verändert: 10.05.2017 10:21
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.

Eigenschaften der Microservices-Architektur

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

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ührt, vor allem wenn es sich um komplexe Anwendungen handelt.

Produktivität und Komplexität

aus Martin Fowler: Microservice Premium

Pro und Contra

Microservices bieten die folgenden Vorteile…

Bessere Modularität
Microservices verstärken modulare Strukturen und verbessern damit die Möglichkeit, in mehreren Teams parallel arbeiten zu können.
Unabhängiges Deployment
Einfache Dienste können einfacher in die Produktion übernommen werden, und da sie autonom sind, sind Systemausfälle weniger wahrscheinlich.
Technologievielfalt:
Mit Microservices können Sie mehrere Sprachen, Frameworks und Datenspeicher einsetzen.

…aber Microservices bringen auch einige Nachteile mit.

Verteilte Systeme
Sie sind schwerer zu programmieren, da Remote-Anrufe langsam sind und mit einem erhöhten Ausfallrisiko verbunden sind
Eventual Consistency
Sowohl die Aufrechterhaltung von starker Konsistenz wie auch Eventual Consistency sind in verteilten Systemen äußerst schwierig.
Operative Komplexität
Die Komplexität zur Verwaltung der Services steigt nicht nur mit der Technologievielfalt sondern auch mit kontinuierlichen Aktualisierungen.

Siehe auch Martin Fowler: Microservice Trade-Offs.