Wie lassen sich Microservice-Muster effizient umsetzen?
Microservices sind aktuell ein viel diskutierter Architekturstil. Damit man nicht wieder bei einem schwer wart- und betreibbaren System aus kleinen Monolithen endet, gibt es einiges zu beachten. Spezielle Designmuster helfen, mehr Struktur in den Entwurf von Microservices zu bringen und dabei Stolperfallen zu vermeiden.
Verteilte Entwicklung und Architekturen gibt es schon seit mehreren Jahrzehnten. So sind für die dort typischerweise auftretenden Herausforderungen bereits Muster und Best Practices beschrieben.
Verteilte Architekturen sind nicht einfach
Zu den Gesetzen der verteilten Entwicklung ("Fallacies of Distributed Computing" von Peter Deutsch [1]) gehört, dass man nur bei konkreten Anforderungen Teile des Systems verteilen soll und wenn man auf Fehlersituationen durch das unzuverlässige Netz vorbereitet ist. Kein Wunder, dass für viele PaaS-Plattformen, wie Microsoft Azure [2] oder Amazon AWS [3] einige Muster für die speziellen Anforderungen der Cloud erweitert wurden. So beschreibt das Whitepaper [4] zum AWS Well-Architected Framework das AWS Cloud Adoption Framework (CAF) und die AWS Cloud Adoption Methodology (CAM) für die vier Konzeptbereiche (Sicherheit, Zuverlässigkeit, Effizienz der Leistung und Kostenoptimierung), um die Designentscheidung beim Erstellen von cloudbasierten AWS-Architekturen zu verbessern oder eine Anwendungsmigration auf die AWS-Plattform zu ermöglichen. Deswegen tut man gut daran, sich mit solchen Mustern zu beschäftigen, bevor man mit der Erstellung von Microservices beginnt.
Der Skalierungswürfel gibt Orientierung
Da es bei Microservices um die Frage nach dem "richtigen Schnitt" geht, ist zunächst zu klären, was das "goldene" Kriterium dafür ist. Oft stellt sich leider erst mit der Zeit und damit im Nachhinein heraus, wo und in welche Richtung die Aufteilung der Dienste sinnvoll und richtig ist. Neben Erfahrung hilft hier die systematische Betrachtung der Zerteil- und Verteilmöglichkeiten am Skalierungswürfel von Abbott und Fisher [5], den diese in ihrem lesenswerten Buch zu "The Art of Scalability" beschrieben haben. Wie der Untertitel "Architecture, Processes, and Organizations" schon andeutet, handelt es sich dabei nicht nur um ein reines Design-, sondern auch um ein Organisations- und Arbeitsproblem.
Die x-Achse wird verwendet, um mit mehr Instanzen oder Hardware die Skalierbarkeit und Verfügbarkeit des Systems zu erhöhen, um so traditionell mit mehr Last umzugehen und konstante Antwortzeiten zu erzielen. Die y-Achse setzt voraus, dass das System nicht nur logisch, sondern auch physisch in separate funktionale Teile zerlegt werden kann. Je nachdem wie abhängig oder isoliert diese voneinander sind, kann das mit einem höheren Kommunikationsaufwand einhergehen. Die höhere Flexibilität im Betrieb geht oft zu Kosten einer höheren Komplexität im Betrieb. Die z-Achse ist ähnlich der x-Achse, da hier Server mit demselben Code jedoch nur mit Teilen der Daten betrieben werden. Neben einem fachlichen Partitionierungsschlüssel muss man sich auch um Konsistenz, Verteilung, Verfügbarkeit oder Replikation der Daten Gedanken machen. Hier hilft das CAP-Theorem ein für seinen Geschäftsvorfall geeignetes Modell für die Daten zu finden.
Da die verschiedenen Skalierungs-Achsen unterschiedliche Auswirkungen auf die Entwicklung und den Betrieb haben, sollte auch aus betriebswirtschaftlichen Gründen genau überlegt werden, welche Qualitätsanforderungen das angestrebte System erfüllen soll.
Aus der Muster-Küche
Es gibt einige Muster, wie z. B. "Enterprise Integration Patterns" (Hohpe, Wolf [6]) oder die "Stability Patterns" (Nygard [7]), die beim Entwurf von Microservices hilfreich sind. Deswegen gibt es für diese bereits Implementierungen in unterschiedliche Programmiersprachen, die deren Verwendung vereinfachen. Jedoch sind für die Verwendung von Microservices "im Großen" noch weitere Muster nötig, die auch die Betriebs- und Verteilungsaspekte berücksichtigen. Ich möchte hier den Microservices-Musterkatalog von Chris Richardson [8], dem Gründer der CloudFoundry-PaaS-Plattform, vorstellen.
Das erste Muster, das oft als Ausgangspunkt oder Gegenpol zu einer Microservices-Architektur gesehen wird, ist der Monolith. Wenn Microservices wachsen, kann es passieren, dass diese, falls keine weiteren Gegenmaßnahmen durchgeführt werden, schnell wieder in einem "kleinen Monolithen" münden. Die Muster lassen sich in die Bereiche Service-Discovery (Client-Side, Server-Side, Service Registry), Kommunikation (API Gateway), Deploymentarten (Mehrinstanzen oder Einzelinstanzen pro Host, Serviceinstanz pro VM oder Container) und Datenhaltung (separate Datenbank pro Service) aufteilen.
Bei den Diensten unterscheidet man zwischen Frontend- (Anwendung) und Backend-(Geschäftslogik) Services. Gerade wenn unterschiedliche Client-Arten, wie Webbrowser, native mobile Anwendungen oder Drittsysteme angebunden werden, ergeben sich unterschiedliche Anforderungen an Granularität, Formate oder Güte des genutzten Dienstes. Statt nur eine universelle Schnittstelle für alle Nutzer anzubieten, sollen spezifische Dienste angeboten werden. Damit Dienste sich nicht direkt kennen und aufrufen, wird eine Vermittlungskomponente benötigt.
Für die Umsetzung wird ein API-Gateway verwendet, als einheitlicher Endpunkt und Proxy für alle Clients. Für die Umsetzung können verschiedene Adapter oder Wrapper angeboten werden, um die eigentliche Implementierung von der Auslieferung zu trennen. Ebenso kann das Gateway sich um weitere Themen, wie Format- und Protokollumwandlung, Autorisierung, Authentifizierung oder Überwachung kümmern. Um die angeforderten, am besten passenden Dienste zu finden, gibt es zwei Arten der Dienstentdeckung auf der Client-Seite oder der Server-Seite. Beide Lösungen setzen voraus, dass sich Dienst-Instanzen selbst bei der Service-Registry an- und abmelden. Das gilt auch im Fehler- oder Überlastungsfall. Beispiele für solche Service-Registries sind Netflix Eureka oder Apache Zookeeper. Dieses Selbstregistrierungsmuster kann auch für die Verwendung von Funktionen von Drittanbietern verwendet werden. Ein gutes Beispiel dafür ist der Netflix OSS Eureka Client.
Die Client-Seite benötigt eine zentrale Service-Registry, in der alle Dienste und ihre Orte verwaltet werden. Hier braucht der Client, der auch das API-Gateway sein kann, nur den entsprechenden Dienst anzufordern und erhält alle Informationen, um diesen aufzurufen. Gerade durch die hohe Anzahl und Dynamik, die mit Microservices verbunden ist, kann das ganze nur automatisch stattfinden. Anders als bei traditionellen Services sind die Orte (Host, Port) der aufgerufenen Dienste nicht fest, sondern können erst zur Laufzeit selbst ermittelt werden und ändern sich auch je nach Verfügbarkeit. Bei der Dienstentdeckung auf dem Server wird die Anfrage nicht an eine Service-Registry, sondern an einen Load-Balancer gestellt. Dieser dient auch als Router zum jeweiligen Dienst und integriert bzw. fragt eine Service-Registry an. Im Gegensatz zur clientseitigen Lösung wird weniger und vor allem einfacher Code benötigt. Dafür muss der Load-Balancer entsprechend ausfallsicher und skalierbar ausgelegt werden. Deswegen gibt es cloudbasierte Produkte, wie AWS Elastic Load Balancer (ELB) oder man verwendet auf jeden Service-Host eine Clusterlösung, wie Kubernetes oder Marathon als lokalen Proxy. Es gibt jedoch auch vorlagenbasierte Lösungen wie Consul, die eine dynamische Umkonfiguration von etablierten Load-Balancern, wie NGINX oder Apache HTTPD ermöglichen. Die anderen Muster beschäftigen sich damit, ob mehrere Services sich einen Host oder ein virtuelles Image teilen. Neben der besseren Ressourcennutzung spricht auch ein einfacheres Management für eine Mehrservice-Lösung.
Je nach Anforderung und Unabhängigkeit der Dienste wird es oft eine Mischung zwischen exklusivem Host-pro-Service und geteiltem Host geben. Je mehr sich jedoch leichtgewichtige Container durchsetzen, wird sich gerade bei Verwendungen heterogener Produkte und Programmiersprachen immer mehr Docker als Lösung für Microservices anbieten. Um einen gemeinsamen Datenbestand für alle Services und damit einen Engpass zu vermeiden, soll jeder Dienst seine eigenen Daten verwalten. Hier wird beim "Database-per-service"-Muster bewusst eine Redundanz von Daten in Kauf genommen. Ebenso kann jeder Dienst die für ihn geeignete Datenbanktechnologie und -produkt wählen. Wenn trotzdem eine einheitliche und konsistente Sicht auf einen Ausschnitt der Gesamtdaten benötigt wird, kann diese mit dem ereignisorientierten CQRS-Muster [9] erreicht werden. Hier kann wieder die z-Achse des Skalierungswürfels angewandt werden, was voraussetzt, dass die Daten entsprechend replizierbar und partitionierbar sind. Eine Voraussetzung für Microservices ist ja, dass die Dienste untereinander nur lose gekoppelt und zustandslos sind. Nur so können diese unabhängig voneinander entwickelt, deployt und skaliert werden.
Fazit
Microservices sind ein sinnvoller Ansatz um auf den wachsenden Bedarf an dynamischen und flexiblen Systemen zu reagieren, wie er für mobile, IoT oder große Webanwendungen benötigt wird. Ein nach den Mustern für Microservices erstelltes System ermöglicht erst, das Potential von Cloud-Angeboten zu nutzen. Hier können die Muster auch zu schrittweisen Ablösungen von Monolithen ein Weg zur Anwendungsmigration [3, 10] in die Cloud sein. Doch die mit einer Erstellung und Betrieb einhergehende Komplexität können auch Microservices nicht wegzaubern. Die vorhandenen verschiedenen Frameworks, Produkte und Werkzeuge sind inzwischen sowohl in der Cloud als auch außerhalb der Cloud verfügbar, um Microservice-Muster effizient umzusetzen und dabei Anfängerfehler zu vermeiden. Seien Sie vorbereitet und kennen Sie die dafür nötigen Muster und Best Practices!
- Wikipedia: Fallacies of Distributed Computing
- Cloud Design Patterns: Prescriptive Architecture Guidance for Cloud Applications patterns & practices, 2014,
- AWS Cloud Design Patterns
- AWS Well-Architected Framework, 2015
Amazon Web Services – A Practical Guide to Cloud Migration, 2015 - Scale Cube: The Art of Scalability: Scalable Web Architecture, Processes, and Organizations for the Modern Enterprise (2nd Edition), Martin L. Abbott, Michael T. Fisher, Addison-Wesley Professional; 2015
- Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions, G. Hohpe, B. Woolf, 2003
- Stability Patterns ...and Antipatterns, M. Nygard, 2012
- Microservice architecture patterns and best practices, Chris Richardson
- Ereignisorientiertes CQRS-Muster/ Seven-part series about Microservices Architecture pattern
- Migrating to Cloud Native Application Architectures, Matt Stine, 2015, O’Reilly, Ch. 3
Publikationen
- Apache Geronimo. Handbuch für den Java-Applikationsserver, m. DVD-ROM: Frank Pientka