Microservices – Architektureigenschaften
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
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.
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.
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.
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.