Während monolithische Applikationen die Tanker der Softwarewelt sind, gleichen modulare Anwendungen eher einer Armada schneller, wendiger Motorboote. Kein Wunder also, dass immer mehr Systeme aus immer kleineren Komponenten bestehen – die sogenannten Microservices. Die Vorteile: Entwickler können dadurch die Entwicklung, das Testing und das Deployment eines Systems unabhängig voneinander durchführen. Ausserdem lässt sich wenig Code besser überblicken und warten. Fällt eine Komponente aus, ist im Idealfall nur eine einzelne Funktionalität betroffen.
So jedenfalls die Theorie. In der Praxis haben diese mehr oder weniger klassisch verteilten Anwendungen auch Nachteile. Wenn einzelne Teile ausfallen oder – schlimmer noch – nur noch teilweise funktionieren, betrifft das oft eben doch das ganze System. Das hat zu neuen Anforderungen an die Softwarearchitektur, die Steuerung eines Netzwerks und das Monitoring geführt.
Cloud Native
Anwendungen, die konsequent auf Microservices und eine Cloud-Infrastruktur setzen, bezeichnet die Fachwelt auch als "Cloud Native". Solche Applikationen setzen konsequent auf die Infrastrukurvorteile der Cloud-Architektur. Container und Containermanager wie Kubernetes sorgen dabei für Ordnung in der Welt der Microservices. Dadurch können Entwicklerteams nicht nur die Entwicklung der Applikationen, des CI und CD sowie die Nutzer- oder Rollenkonzepte harmonisieren. Auch optimieren sie die Kommunikation der verschiedenen Module einer Applikation miteinander.
Als Plattform ist Kubernetes quasi zum Standard für die Verwaltung von Applikationen in einem Cluster geworden. Es macht im Wesentlichen nichts anderes, als Container zu starten, zu überwachen, gegebenenfalls wieder zu beenden und mit einem Cluster von Knoten zu arbeiten. Dabei sorgt es auch für eine Vernetzung der Container. Die kleinste Einheit ist allerdings kein Container, sondern ein Pod – also ein Behälter für einen oder mehrere Container. Die Container eines Pods teilen sich Ressourcen wie Volumes oder die IP-Adresse. Dies ermöglicht einige interessante Optionen, um Applikationen zu modularisieren: Ähnlich wie bei den aus der OOP bekannten Patterns gibt es hier sogenannte Container-Patterns. Dieses Konzept machen sich auch die Service-Meshes zunutze.
Was ist ein Service-Mesh?
Service-Meshes sind steuerbare Komponenten, die sich zwischen die Service-zu-Service-Kommunikation einer Applikation schalten. Dies muss nicht zwangsläufig in einem Kubernetes Cluster passieren, ist dort aber besonders einfach. Service-Meshes nutzen dazu die Tatsache, dass sich Pods mithilfe gängiger Container-Patterns flexibel erweitern lassen. Dies kann etwa in Form eines Sidecars geschehen, was ein zusätzlicher Container in einem Pod ist, der als Proxy dient. Der Proxy kann als Teil eines Service-Meshs die gesamte Kommunikation kontrollieren und lässt sich durch ein sogenanntes Control Plane steuern. Dadurch kann ein Service-Mesh folgende Fragen beantworten:
- Wer spricht mit wem?
- Wie lässt sich die Kommunikation notfalls einschränken?
- Wie lässt sich die Kommunikation wechselseitig verschlüsseln?
- Welche Kette an Services ist an einer bestimmten Transaktion beteiligt?
- Wie lässt sich der Traffic feingranular steuern?
- Wie lässt sich zum Beispiel die Bereitstellung von der Inbetriebnahme trennen?
- Wie lässt sich die Widerstandsfähigkeit der Applikation erhöhen?
Dazu nutzt ein Service-Mesh eine Reihe von Massnahmen wie automatisches Monitoring, Traffic-Management oder Multicluster. Im Folgenden gibt es einen Überblick über die wichtigsten Massnahmen:
Automatisches Monitoring
Ein Service-Mesh kann die gesamte Kommunikation über die Proxies in einem Monitoring-System wie Prometheus protokollieren. Dadurch lässt sich beispielsweise auswerten, wie, wann oder mit welcher Latenz die Services miteinander kommunizieren. Es ist oft wertvoll, zu wissen, welche Services im realen Umfeld überhaupt miteinander kommunizieren. Alleine aus dem, was deployed ist, können Entwickler das als eine Art statische Analyse schlecht erkennen. Ebenso interessant ist es für Entwickler zu sehen, welche Fehler es bei der Kommunikation gibt, wie hoch die Fehlerrate ist und welche Services die Ursache dafür sind. Und schliesslich hilft Entwicklern ein schneller und einfacher Überblick über die Durchlaufzeiten und die beteiligten Services einer komplexen Transaktion, die durch mehrere unterschiedliche Services ausgeführt wird.
Traffic-Management
Kubernetes regelt den Zugriff auf Pods durch sogenannte Serviceressourcen. Dabei lassen sich im wesentlichen nur Rundlaufverfahren (Round Robin) anwenden, bei denen die Prozesse in einer Warteschlange nacheinander ablaufen. Der prozentuale Anteil des Traffics ist somit durch die Anzahl der Instanzen eines Deployments bestimmt. Eine freie Steuerung des Traffics – etwa auf Basis von Nutzernamen oder Herkunft – ist nicht möglich. Ein Service-Mesh bietet hier wesentlich mehr Möglichkeiten.
Sicherheitsmassnahmen
Die Aufteilung einer monolithischen Applikation in einzelne Services – die über Netzwerke und nicht in einem einzelnen Prozess miteinander kommunizieren – bringen auch andere Anforderungen an die Sicherheit mit sich. Um sich zum Beispiel gegen einen Man-in-the-Middle-Angriff zu schützen, bei dem der Angreifer zwischen zwei Kommunikationspartnern steht, muss die Kommunikation verschlüsselt sein. Das gilt besonders, wenn Entwickler einem Netzwerk nicht trauen können. Darüber hinaus sind anpassbare Richtlinien für die Zugriffe und eine flexible Zugangskontrolle notwendig. Dazu gehört zum Beispiel Mutual TLS, mit dem sich Dienste gleichzeitig gegenseitig authentifizieren können. Wer prüfen möchte, wer was zu welchem Zeitpunkt getan hat, benötigt ausserdem Audit-Werkzeuge.
Multicluster
In vielen Szenarien müssen Entwickler externe oder Legacy-Systeme in eine Anwendung einbauen. Ausserdem befinden sich manchmal Teile einer grösseren Applikation in mehr als einem Kubernetes-Cluster. Für solche Fälle lassen sich Service-Meshes auch über Cluster- und Systemgrenzen hinweg spannen.
Robustere Infrastruktur
Wie bereits erwähnt, können bei komplexen Architekturen mit vielen kleinen Funktionsservices Fehler in diesen Ketten von Aufrufen entstehen. Das kann wiederum zu Folgefehlern führen. Service-Meshes versuchen die Auswirkungen solcher Fehler durch sogenannte Resilience Patterns zu minimieren, damit es nicht zu einem Ausfall der gesamten Anwendung kommt.
Anwendungsfälle
Die Vorteile von Service-Meshes treten zum einen in der Betriebsphase einer Applikation auf, zum anderen während der Software-Entwicklung. Für den Betrieb einer Applikation sind die Ressourcen von Kubernetes in vielen Fällen ausreichend. Ist eine Anwendung jedoch komplexer und läuft sie über einen längeren Zeitraum, ist ein flexibleres und dynamisches Routing nötig. Auf diese Weise lässt sich zum Beispiel eine neue Version prozentual oder anhand gewisser Parameter (etwa nur für bestimmte Mobilgeräte, Browser oder Nutzer) ausspielen. So können Entwickler den Anteil des Traffics – vielleicht über mehrere Tage hinweg und unter ständiger Beobachtung der technischen und fachlichen Metriken – schrittweise von 0 auf 100 Prozent erhöhen. Mit einem Service-Mesh geht das leichtgewichtig, ohne dass ein Entwickler irgendeinen Programmcode oder ein Deployment verändern muss. Service-Meshes sind in der Betriebsphase aber auch dann von Vorteil, wenn eine Software Mängel aufweist. Solche einzelnen Fehler oder grösser werdende Latenzen sollen bei einer produktiven Anwendung natürlich nicht zum Komplettausfall führen. Um dies zu verhindern oder zumindest die Wahrscheinlichkeit zu verringern, können Entwickler die Requests pro Sekunde dynamisch limitieren oder ein Circuit-Breaker-Pattern einbringen. Mit so einem Resilience-Pattern können Entwickler wiederkehrende Verbindungsfehler einfacher handhaben. Beides lässt sich dynamisch parametrisieren, ohne den Programmcode zu verändern.
Während der Softwareentwicklung bringen Service-Meshes vor allem bei der Simulation von Nutzerverhalten entscheidende Vorteile: Wenn zum Beispiel das Nutzerverhalten schwer vorherzusagen ist, lassen sich auch das Verhalten einer Applikation und die daraus eventuell resultierenden erhöhten Latenzen schlecht testen. Hier kann ein Service-Mesh beim Test den realen Live-Traffic einer Applikation abzweigen und auf ein Test-Deployment leiten. Auch Fehler, die durch die erhöhten Latenzen anderer Services entstehen, können Entwickler durch die sogenannte Error-Injektion an bestimmten Stellen der Aufrufkette simulieren.
Aktuelle Service-Meshes
Mittlerweile gibt es mehrere Service-Meshes, die unter anderem auf Kubernetes optimiert sind. Der klare Platzhirsch ist hierbei Istio. Teams von Google und IBM haben das Opensource-Projekt auf GitHub in Zusammenarbeit mit dem Envoy-Team von Lyft gestartet. Die Mitarbeit von Google ist wohl auch der Grund dafür, dass sich Istio inzwischen für Googles Kubernetes-Engine auswählen lässt. Eine einfachere Installation gibt es nicht. Dennoch war Istio in der Vergangenheit ein ziemlich grosser Satz sich bewegender Teile. Erst mit der Version 1 zeigt sie sich ein wenig modularisiert und als Ganzes etwas handlicher.
Ein weiteres Service-Mesh ist Linkerd von Buoyant. Es existierte schon vor Frameworks wie Kubernetes. Linkerd 2 will diese Erfahrungen nun als Service-Mesh für Kubernetes umsetzen und dabei dessen Möglichkeiten nutzen. Laut Buoyant ist Linkerd 2 nicht nur ultraleicht und ultraschnell, sondern auch ein "<No-Hype-Service-Mesh". Das "No-Hype" bezieht sich wohl darauf, dass Linkerd zwar weniger Funktionen hat, diese aber ausgereifter und performanter sind oder sein werden. Diese Einschätzung ist auch nicht ganz falsch – Istio hatte in der Vergangenheit öfter Qualitäts- und Latenzprobleme.
Cilium ist kein Service-Mesh, sondern ein Netzwerk-Plugin und setzt an einer ganz anderen Stelle an als Istio und Linkerd. Cilium soll Netzwerkverbindung zwischen Services transparent sichern – also damit eigentlich ein Netzwerk. Sicherlich lassen sich damit nicht alle Funktionen eines Service-Meshs realisieren, aber einige wichtige. Dazu gehören die transparente Verschlüsselung, L7 Firewalls, das Sammeln von Metriken, die Verbindung mehrerer Cluster oder die sichere Kommunikation zwischen Diensten basierend auf Identitäten. Cilium nutzt dabei Funktionen des Linux Kernels wie BPF, was es besonders latenzarm und performant machen soll.
Herausforderungen in der Anwendung
So gut das alles klingt, Entwickler müssen sich darüber im Klaren sein, dass sie mit einem Service-Mesh immer auch eine grosse Menge weiterer Komponenten zu einem Cluster hinzufügen (etwa Admission Controller, Control Plane oder Proxies und vieles mehr). Sicherlich ist von Vorteil, dass sich zum Beispiel Istio als eine Art gesonderte Infrastrukturkomponente ansehen lässt, so wie es das Cloud-Native-Paradigma vorschreibt. Dennoch müssen Entwickler diese betreiben, updaten und so weiter. Um die Anforderung zu erfüllen, dass Proxy-Container möglichst transparent einzubringen sind, sind darüber hinaus teilweise gesonderte Rechte für die Proxy-Container notwendig. Diese möchten Entwickler aber nicht immer unbedingt erlauben. Dennoch bleibt ihnen oft nichts anderes übrig, wenn sie Service-Meshes einsetzen. Dass sie dann den gesamten Traffic über Proxies leiten müssen, bedeutet zudem eine höhere Latenz bei den Zugriffen. Das kann eine stark verteilte Anwendung schon vor Probleme stellen.