Die Minions der Softwareentwicklung: Wie Microservices funktionieren und was sie leisten

Wer schon einmal eine selbst entwickelte Software über längere Zeit betreut, gewartet und weiterentwickelt hat, kennt das Problem: Bugfixes, neue oder sich verändernde Anforderungen und stetiger technologischer Wandel sorgen dafür, dass Software kontinuierlich wächst und sich verändert. Ohne entsprechende Maßnahmen führt diese „Software-Evolution“ häufig dazu, dass Software größer und komplexer, und somit auch schwieriger zu warten wird.
Um dieser „Softwarealterung“ vorzubeugen, hat sich in den vergangenen Jahren mit dem Microservice-Muster ein neues Architekturparadigma etabliert. Dieses versucht, die Komplexität großer, wachsender Softwaresysteme besser beherrschbar zu machen und somit eine einfache Wartbarkeit sicherzustellen.
Wie funktionieren Microservices?
Eine Microservice-Architektur teilt ein Softwaresystem in eine Vielzahl einzelner, kleiner und unabhängiger Dienste auf. Diese laufen als einzelne Prozesse (somit in der Regel auch über ein Netzwerk verteilt) und kommunizieren über einfache Netzwerkschnittstellen miteinander.
Gegenüber einer herkömmlichen „monolithisch“ aufgebauten Software bietet diese Architektur einige Vorteile – beispielsweise können einzelne Services unterschiedliche Technologien nutzen. Während sich für einen Service womöglich Java als Laufzeitumgebung und MySQL als Datenbankmanagementsystem eignen, sind für einen anderen Service womöglich Node.js und eine Mongo-Datenbank bessere Kandidaten. Die Kapselung in einzelne Services ermöglicht es Entwicklungsteams somit, das jeweils beste Werkzeug für den jeweiligen Zweck zu wählen.
Die Aufteilung in mehrere Einzelkomponenten kann zudem die Auslieferung neuer Versionen vereinfachen: Statt das komplette System am Stück zu deployen (gerade bei komplexen Softwaresystemen ein womöglich riskantes Vorhaben), können einzelne Services nun unabhängig voneinander ausgeliefert werden (oder sogar durch Neu-Implementierungen ersetzt werden).
Im Unterschied zu herkömmlichen serviceorientierten Architekturen (SOAs) liegt der Fokus bei Microservice-Architekturen auf Einfachheit. Der Funktionsumfang eines einzelnen Services sollte möglichst überschaubar sein. Außerdem werden einfachere Kommunikationsprotokolle wichtiger. Statt des schwergewichtigen SOAP-Protokolls geht der Trend zu einfacheren Kommunikationsmustern wie RESTful HTTP, oder – falls Asynchronität oder eine Publish/Subscribe-Architektur gewünscht ist – auch AMQP.
Ein Beispiel für eine Microservice-Architektur im E-Commerce: Sie besteht aus vier verschiedenen Microservices, jeweils mit eigener Datenhaltung, drei externen Applikationen, und einem Gateway, welches den Zugriff auf die verschiedenen Schnittstellen der Services ermöglicht und kontrolliert. Die obige Abbildung zeigt eine Beispiel-Architektur für ein E-Commerce-Szenario, bestehend aus vier Microservices, die verschiedene Aufgaben übernehmen.
Üblicherweise ist in einer Microservice-Architektur jeder Service selbst für seine eigene Datenhaltung zuständig. Statt „der“ zentralen Datenbank gibt es in einer Microservice-Architektur somit viele verschiedene Datenquellen. Dies mag auf den ersten Blick nicht gerade intuitiv sein, da sich augenscheinlich die Komplexität des Systems erhöht. Würden jedoch mehrere Services eine gemeinsame Datenbank nutzen, wäre dies eine versteckte Abhängigkeit der Services untereinander. Vorzugsweise sollten jedoch alle Abhängigkeiten der Services untereinander explizit über APIs ablaufen und jeder davon für seine eigene Datenhaltung verantwortlich sein. Nur auf diese Weise ist jeder Service tatsächlich unabhängig von den anderen und kann die jeweils optimale Art der Datenspeicherung nutzen.
Um eine externe API anbieten zu können (die dann beispielsweise von Mobil-Applikationen, externen Partnern oder auch eigenen Anwendungen genutzt werden können), erfolgt der externe Zugriff auf die APIs einer Microservice-Architektur üblicherweise über ein Gateway. Dieses stellt eine einheitliche Schnittstelle für alle Services zur Verfügung und ermöglicht die Übernahme von Querschnittsaufgaben wie beispielsweise Authentifizierung für externe Nutzer, Protokollierung und Rate-Limiting.
Komplexität managen
Der Vorteil dieses Ansatzes: Jeder einzelne Service hat einen überschaubaren Funktionsumfang und keine besondere Komplexität. Dies kommt der Test- und Wartbarkeit der einzelnen Komponenten zugute. Als Softwarearchitekt oder -entwickler darf man sich jedoch nun nicht dem Trugschluss hingeben, dass sich auf diese Weise die Komplexität des Gesamtsystems reduzieren lässt. Insgesamt erhöht sich die Gesamt-Komplexität des Systems innerhalb einer Microservice-Architektur eventuell sogar, da häufig viele verschiedene Technologien zum Einsatz kommen und die Auslieferung der Software zwar pro Service einfacher, in Summe jedoch komplexer wird. Durch die Kommunikation über Netzwerke kommen zudem weitere Fehlerquellen hinzu, die bei der Gestaltung des Systems entsprechend zu berücksichtigen sind.
Diesem Komplexitätsanstieg lässt sich durch den konsequenten Einsatz von Automatisierung entgegengewirken. Geeignete Werkzeuge können den kompletten Lebenszyklus eines Microservice vom Kompilieren, Paketieren (beispielsweise in Container-Images), Testen bis hin zur Auslieferung so weit automatisieren, dass eine kontinuierliche Auslieferung von Software möglich ist. Infrastruktur sollte, soweit möglich, in Form von Quelltext abgebildet und über Konfigurationsmanagementsysteme im Soll-Zustand gehalten werden – auf diese Weise funktioniert das Ausrollen von Infrastruktur-Änderungen vollkommen automatisch.
Häufige Herausforderungen bei Microservices
Schnittstellen
Das Kernstück eines jeden Microservices ist seine externe API, über die andere Services oder externe Nutzer ihn ansprechen können. Als De-facto-Standard hat sich hier das Programmier-Paradigma RESTful HTTP herauskristallisiert.
Um die Entwicklung schlanker REST-Webservices zu unterstützen, haben sich in zahlreichen Sprachen so genannte Micro-Frameworks entwickelt. Diese erlauben es, mit nur wenigen Zeilen Code vollwertige REST-Services zu implementieren, und kommen ohne den Konfigurationsaufwand „großer“ Full-Stack-Frameworks wie Symfony (PHP) oder Spring (Java) aus. Nennenswerte Micro-Frameworks sind beispielsweise die Slim- und Silex-Frameworks (PHP), Express (Node.JS) oder Spark (Java).
Neben REST über HTTP eignen sich auch asynchrone Kommunikationsprotokolle wie das Advanced Message Queuing Procotol (kurz genannt AMQP) sehr gut, um Services untereinander zu integrieren. Die Nutzung von AMQP ermöglicht beispielsweise die Implementierung einer Publish/Subscribe-Architektur. Zentrale Komponente in einer solchen Architektur ist ein so genannter Message Broker (eine beliebte Implementierung ist beispielsweise RabbitMQ, in welcher ein Service Nachrichten bei einem Message-Broker veröffentlichen kann; andere Services lassen sich dann am Broker für eine automatische Zustellung dieser Nachrichten registrieren.
Auf diese Weise werden der veröffentlichende und empfangende Service voneinander entkoppelt – dies geht so weit, dass der Sender gar nicht wissen muss, an wen er die Nachrichten später zustellen muss.
Deployment
Die Verteilung einzelner Services auf einzelne Server ist in Microservice-Architekturen komplexer, da nun nicht nur eine Applikation verteilt werden muss, sondern viele. Außerdem bauen diese womöglich auf verschiedenen Technologien auf. Als Antwort auf diese Herausforderung haben sich Container-Lösungen wie Docker oder rkt etabliert. Damit lassen sich Anwendungen mitsamt ihren Abhängigkeiten (wie etwa Laufzeitumgebungen und Bibliotheken) in Containern verpacken, die dann einfach ausgeliefert werden können.
Die Verteilung solcher Container über mehrere Server wird zudem von Cluster-Managern wie Mesos, Kubernetes oder Swarm unterstützt. Allen Lösungen ist gemein, dass sie einen sehr hohen Grad der Automatisierung ermöglichen. Hierzu gehört beispielsweise, dass der Cluster-Manager die tatsächliche Verteilung von Containern auf die physischen Server übernimmt und Container automatisch dort platziert, wo noch entsprechende Kapazitäten frei sind.
Viele Lösungen übernehmen auch eine automatische Skalierung, um unter Last weitere Instanzen eines Services zu starten. Über einen etwas erweiterten Continuous-Delivery-Workflow lässt sich somit – ausgelöst durch eine Quelltextänderung in der Versionskontrolle – der gesamte Prozess vom Bauen eines Container-Abbilds, dessen Integrationstest sowie der Auslieferung der neuen Version einer Software über hunderte von Servern vollständig automatisieren.
Service Discovery
Ein Microservice ist nicht sonderlich nützlich, wenn er nicht für andere Services ansprechbar ist. Hierzu müssen Service-Konsumenten jedoch wissen, wo und auf welche Weise sie den Service erreichen. Dieses Problem nennt sich „Service Discovery“, also das Auffinden von Diensten in einer komplexen Architektur. Es ist insbesondere in hochdynamischen Architekturen wichtig, in denen beispielsweise ein Cluster-Manager wie Kubernetes abhängig von der aktuellen Last und nach eigenem Ermessen neue Instanzen von Services startet und beendet.
Service Discovery in einer (micro)serviceorientierten Architektur: In der zentralen Service Registry registrieren sich alle laufenden Dienste. Sie gibt darauf die Adressen der laufenden Instanzen an andere Dienste zurück, die einen bestimmten Dienst nutzen wollen.
Die Abbildung zeigt eine abstrahierte Form einer (micro-)serviceorientierten Architektur mit Service Discovery. Zentrale Komponente ist die Service Registry, in der sich laufende Dienste registrieren (1). Möchten andere Dienste nun einen bestimmten Dienst nutzen, so können sie bei der Registry nach gerade laufenden Instanzen dieses Dienstes anfragen (2). Die Service Registry liefert dann eine oder mehrere Adressen (beispielsweise HTTP-URLs) zurück (3), unter denen der gewünschte Dienst erreichbar ist (4).
Als Lösungen haben sich hier vor allem Zookeeper und Consul etabliert. Insbesondere Consul erfreut sich aufgrund seiner Einfachheit und guter Integrationsmöglichkeiten zunehmender Beliebtheit und arbeitet gut mit Container- und Clustermanagern zusammen. So lassen sich beispielsweise neu gestartete Docker-Container automatisch als Service in Consul registrieren, um dort für andere Services zur Verfügung zu stehen.
Ausblick
Entwickler-Teams sollten Microservices nicht unreflektiert anwenden, da dies auch Risiken mit sich bringt. Durch die Aufteilung einer monolithischen Applikation in mehrere Microservices verlagert sich die Komplexität des Gesamtsystems von der Anwendungssoftware in die Orchestrierung der einzelnen Services. Um diese beherrschbar zu machen, ist der konsequente Einsatz von Automatisierung und Continuous Delivery nötig. Die Verteilung eines Systems über ein Netzwerk führt zudem neue mögliche Fehlerquellen ein, die es bei der Programmierung zu berücksichtigen gilt.
Werden diese Fallstricke berücksichtigt, ist die Microservice-Architektur ein guter Ansatz, um der steigenden Komplexität wachsender Softwaresysteme Herr zu werden. Der Ansatz ermöglicht es, eine Software schnell an sich verändernde Anforderungen anzupassen und zeitnah neue Technologien einzusetzen. Zudem sind so einzelne Teile der Applikation einfach skalierbar.
Darüber hinaus funktionieren Microservice-Architekturen sehr gut in Organisationen, die nach dem DevOps-Prinzip arbeiten, da einzelne Produktteams nun einen einzelnen Service über dessen kompletten Lebenszyklus betreuen und somit unabhängig und agil arbeiten können.