Wer sich schon länger mit Software-Entwicklung beschäftigt, kennt den monolithischen Ansatz. Der besteht darin, dass eine Software als Einheit angelegt wird. Alle funktionalen Komponenten werden in dieser Einheit verwaltet. Alles befindet sich in einer einzigen Code-Basis. Egal, ob es sich um die Geschäftslogik, die Datenbankoperationen oder die Hintergrundprozesse handelt.
Früher: der monolithische Ansatz
Wenn Aktualisierungen vorgenommen werden, kann es zu Wechselwirkungen kommen. Skalierung ist schwierig. An einem Punkt X wird die Anwendung zu komplex, um sie noch sinnvoll zu handhaben. Das Zufügen neuer Funktionen wird zunehmend schwieriger, weil ständig Korrelationen bedacht werden müssen. Ideen werden möglichst vermieden, um nicht Gefahr zu laufen, sie auch umsetzen zu müssen.
Auch das Troubleshooting gestaltet sich schwierig. Wie bei den früher üblichen Kompaktanlagen, bei denen das komplette Gerät zum Service musste, bloß weil das Tape-Deck ausgefallen war, verhält es sich auch bei Software-Monolithen. Ein Fehler in Routine X kann das gesamte System lahmlegen.
Objektorientierte Ansätze mildern viele Probleme mit dem monolithischen Ansatz, können aber nicht darüber hinwegtäuschen, dass es immer noch ein monolithischer Ansatz bleibt. Bei Software, die über das Web verwendet wird, erweist sich der Ansatz spätestens dann als problematisch, wenn Skalierung erforderlich wird – etwa um einer gestiegenen Nutzerlast zu begegnen.
Ähnlich wie bei einer SQL-Datenbank, die leistungsfähiger werden soll, muss zunächst die konkrete Hardware, auf der das System läuft, bestmöglich ertüchtigt, also aufgerüstet werden. Im Datenbank-Gewerbe hat eben diese Problematik den Siegeszug der NoSQL-Datenbanken ausgelöst. Die skalieren dadurch, dass eine weitere Instanz, also ein weiterer Rechner ins Netz eingebunden wird. Alles Weitere regelt die NoSQL-Architektur selbst. So wird die Datenbank umso leistungsfähiger, je mehr Rechner beteiligt sind. Die konkrete Leistungsfähigkeit des einzelnen Rechners ist nicht völlig unbeachtlich, aber deutlich weniger relevant als bei monolithischen Datenbank-Systemen.
Heute: Microservices
Einen ähnlichen Ansatz verfolgt das Konzept der Microservices. Statt aus einer monolithischen Code-Basis eine Monumental-App mit allen Features zu kompilieren, setzen wir eine maximale Vielzahl unabhängiger Funktionsteile miteinander in Beziehung.
Jedes Funktionsteil ist dabei als Service angelegt. Die Bezeichnung „Micro“ weist darauf hin, dass die Einheit sehr klein gehalten wird. Wir bauen uns also eine Palette an Spezialisten zusammen, die sich gegenseitig per API ansprechen und nutzen. Da die API der einzige Punkt ist, an dem sich die Microservices berühren, herrscht maximale Flexibilität in der Umsetzung.
Vorteile von Microservices
Große Teams können sich unproblematisch aufteilen, um an ihren jeweiligen Microservices zu arbeiten. Koordinationsbedarf gibt es natürlich trotzdem; der bezieht sich aber ausschließlich auf die Funktionen der API. Deshalb kann jedes Unter-Team zunächst nahezu frei und ohne auf andere Projekteilnehmer warten zu müssen, entwickeln. Das fördert die Agilität – ein wichtiges Kriterium für moderne Entwicklerteams. Definiert werden muss nur, was die API leisten soll. Dann kann jedes Unter-Team aber sogar eigene Entscheidungen zum Tech-Stack treffen und etwa einen Dienst in einer Sprache oder auf der Basis eines Frameworks umsetzen, das ansonsten nirgends im Projekt verwendet wird.
Das Anbinden neuer Funktionen ist relativ einfach. Ein weiterer Microservice muss geschaffen werden, der per API angebunden wird. Veraltete Funktionsteile können durch modernere Ansätze ersetzt werden, ohne irgendwelche anderen Funktionsteile zu berühren, geschweige denn zu beeinträchtigen. Skalieren können Microservices geradezu nach Belieben. Sie müssen immerhin nicht auf einer Instanz mit anderen Diensten laufen, sondern können die Ressourcen erhalten, die sie benötigen.
Ein weiterer Vorteil der Microservices besteht darin, dass sie nicht auf ein einzelnes Projekt begrenzt sind, sondern überall da eingebunden werden können, wo es die Entwicklung für sinnvoll erachten lässt. Sie stellen quasi ein eigenes Infrastrukturangebot dar.
Der Microservices-Ansatz ist zudem die Voraussetzung für die Umsetzung einer Strategie des Continuous-Delivery. Bei dieser Methode werden beständig neue Versionen der Software ausgeliefert. Diesen Ansatz und die Continuous-Integration als etwas weniger invasives Konzept haben wir ausführlicher an dieser Stelle beschrieben.
Nachteile von Microservices
Wer in stark delegierten Umgebungen arbeitet, erkennt neben den Vorteilen des freien Schaffens in der eigenen Verantwortlichkeitsblase auch einen gravierenden Nachteil: Da es letztlich immer um einen gemeinsamen, geradezu ganzheitlichen Projekterfolg gehen wird, ist ein Erfolg im eigenen Kleinen nicht das, worauf es am Ende ankommt. Vielmehr muss dann doch wieder das große Ganze stimmen. So ist es natürlich auch bei Microservice-basierten Projekten. Am Ende baut nicht jedes Team seinen Microservice und ist fertig damit. Wichtig ist, dass das Gesamtprojekt sauber umgesetzt dasteht. Für Teams, die sich für die Microservices-Architektur entschieden haben, bedeutet das einen nicht unerheblichen Koordinationsaufwand an den Schnittstellen. Deshalb ist es für den Erfolg des Projekts unerlässlich, die Kommunikation der Dienste untereinander so dezidiert wie möglich zu definieren. Wer stellt welche Anfrage wann und wieso und wie wird diese Anfrage wie schnell beantwortet? Das wäre schon mal ein guter Anfang.
Richtig gemacht sind Microservices self-contained, beinhalten also alles, was sie für die eigene Existenz benötigen. Das betrifft alle Ressourcen – vor allem auch alle Lösungen zur Datenhaltung. Dadurch entsteht eine sehr fragmentierte Datenstruktur mit einer sehr hohen Zahl an Transaktionen und mit ziemlicher Sicherheit auch dem Bedarf nach einer Konsolidierung. Das ist ein komplexes Thema, das nicht vernachlässigt werden darf.
Da jeder Dienst separat läuft, muss deren Funktionsfähigkeit separat überwacht werden. Auch das Deployment, also die Bereitstellung für den Betrieb, erfolgt individuell für jeden Funktionsteil. Das bedeutet mehr Arbeitszeit als für die Bereitstellung und das Monitoring einer monolithischen Anwendung. Für das Deployment muss auch aus einem anderen Grund deutlich mehr Zeit eingeplant werden: Die Schnittstellen müssen einzeln geprüft werden und zwar im Zusammenspiel mit allen Services, die dazu vorgesehen sind. Bei vielen Services kommen dabei Testszenarien zustande, die echte Fleißarbeit nach sich ziehen.
Die Nachteile wiegen allerdings die Vorteile nicht auf. So bietet sich im Grunde fast immer die Verwendung einer Microservices-Architektur an – es sei denn, der geplante Monolith hat einen so beschränkten Funktionsumfang, dass die Aufteilung in Funktionseinheiten wie das berühmte Mit-Kanonen-auf-Spatzen-Schießen wirkt. Dann aber ist der Monolith selbst nicht viel mehr als ein Microservice.
Deployment von Microservices mit Docker und Kubernetes
Microservices werden in der Regel in Containern bereitgestellt. Dabei handelt es sich um virtuelle Betriebssystemumgebungen, auf denen exklusiv der jeweilige Microservice läuft.
Eine der beliebtesten dieser Lösungen ist Docker. Mit Docker wird jeder Microservice in ein Docker-Image und einen Docker-Container verpackt. Diese Teile sind völlig unabhängig von der Host-Umgebung. Docker ersetzt dabei die Notwendigkeit, eine eigene virtuelle Betriebssystemumgebung zu haben, indem der Kernel des Betriebssystems auf seinem Docker-Host freigegeben wird. So können Microservices in noch kleinere Code-Stücke aufgeteilt und als Docker-Images über Dateien namens Dockerfiles erstellt werden. Diese Dockerfiles machen das Einbinden von Microservices in eine große Anwendung wesentlich einfacher.
Microservice-Lösungen werden in der Regel aus mehreren Docker-Containern aufgebaut, die über das virtuelle Netzwerk koordiniert werden. Um die erforderliche Kommunikation der Container untereinander zu gewährleisten, kommt die Umgebung Docker Compose zum Einsatz.
Noch feiner orchestriert werden können die Microservice-Umgebungen mit Kubernetes. Mit Kubernetes lassen sich Container zu Clustern zusammenführen, die sich gemeinsam verwalten lassen. Kubernetes kann Container-Operationen rund um die Vernetzung, Sicherheit, Skalierung und weitere automatisieren. Wer den Einsatz von Kubernetes erwägt, sollte allerdings bedenken, dass das Orchestrierungs-Tool die Unterstützung für Docker irgendwann im Jahr 2021 beenden wird. Für das saubere Zusammenspiel sollte dann auf eine zum Kubernetes-CRI (Container Runtime Interface) kompatible Runtime, etwa Containerd, gewechselt werden.
Probleme, die es vorher nicht gab
Besonders der Umstand, dass große Microservices-Umgebungen eine Vielzahl an Schnittstellen und eine entsprechend große Zahl an Transaktionen untereinander abwickeln, erhöht den Wartungsaufwand deutlich. Fehler sind nicht so leicht zu erkennen. Entsprechend braucht es gute Hilfsmittel, die euch helfen, das Muster im Kommunikationswirrwarr zu erkennen.
Service-Mesh
Ein wichtiges Element im Bemühen, die Kontrolle zu behalten oder wiederzuerlangen, ist das sogenannte Service-Mesh. Das setzt sich in den Kommunikationsspalt der Microservices und überwacht dort die API und alles, was über sie abgewickelt wird. Dadurch kann sie umfangreiche Informationen zur potenziellen Optimierung bieten und ist letztlich in der Lage, kontrollierend einzugreifen. Zu Service-Meshes haben wir einen ausführlichen Ratgeber für euch. Als Platzhirsch in diesem Bereich gilt das von Google und IBM gemeinsam als Open Source entwickelte Istio.
Tracing
Wem es nur darum geht, Fehler in der Kommunikation der Microservices untereinander zu finden, für den bieten sich sogenannte Tracer an. Auch ein Tracer, wie etwa Jaeger, kann protokollieren, welcher Service mit welchem anderen kommuniziert. Hauptsächlich werden Tracer indes zum Aufspüren von Fehlern und – noch wichtiger – Performanzproblemen verwendet, die etwa dann entstehen können, wenn sich Microservices gegenseitig aufrufen. Auch zum Tracing in Microservice-Architekturen haben wir einen ausführlichen Ratgeber für euch.
Service-Discovery
Wer in einer hochdynamischen Umgebung etwa Kubernetes nutzt, hat damit eine Lösung, die im Zweifel lastabhängig oder aufgrund anderer Konditionen Instanzen von Microservices startet oder beendet. Das kann zur Folge haben, dass Schnittstellenanfragen nicht korrekt beantwortet werden – etwa weil die angefragte Instanz des Services eben beendet wurde.
Um dieses Problem zu umgehen, gibt es Lösungen zum sogenannten Service-Discovery. Software-Lösungen wie Consul errichten dabei eine Registry, in der sich laufende Dienste anmelden. Wollen andere Dienste nun einen bestimmten Dienst nutzen, so können sie in der Registry die gerade laufenden Instanzen dieses Dienstes anfragen und erhalten eine oder mehrere Adressen geliefert, unter denen der gewünschte Dienst ansprechbar ist. Das erwähnte Consul ist übrigens auch in der Lage, ein Service-Mesh zu errichten.