Es gibt sehr viele unterschiedliche Technologien für die Implementierung von Microservices-Architekturen [3], [4]. Kubernetes [5] ist gerade für den Betrieb von Microservices eine hervorragende Infrastruktur: Docker-Container laufen nicht mehr auf einzelnen Knoten, sondern im Cluster; das erlaubt Ausfallsicherheit und Skalierung. Außerdem hilft Kubernetes beim Deployment der Anwendungen. Durch Rolling-Updates können neue Versionen schrittweise eingeführt werden, indem Container mit der neuen Version gestartet und dann die Container mit der alten Version entfernt werden.
Außerdem ermöglicht Kubernetes Service Discovery: Container können andere Container anhand des DNS-Namens finden. DNS dient auch im Internet zum Auflösen von Hostnamen zu IP-Adressen und hat daher eine breite Unterstützung über viele Programmiersprachen hinweg. Load Balancing löst Kubernetes ebenfalls: Die Last kann auf mehrere Instanzen verteilt werden; dabei wird der Netzwerkverkehr transparent auf IP-Ebene umgeleitet.
Aus diesen Gründen ist Kubernetes mittlerweile eine der wichtigsten Plattformen für Microservices: Ohne Eingriffe in den Code werden wichtige Herausforderungen für Microservices gelöst. So bleibt die Technologiefreiheit bestehen. Aber Kubernetes löst noch nicht alle Herausforderungen von Microservices.
LUST AUF MEHR SOFTWARE ARCHITEKTUR?
Zahlreiche aktuelle Themen wie KI, LLMS und Machine Learning, sowie Frontend-Architektur, für Tools zur Softwarearchitektur, Cloudlösungen und Software beweisen.
Service Mesh: Proxies im Einsatz
Der Zweck von Kubernetes ist der zuverlässige Betrieb von beliebigen Containern. Der Orchestrierer bildet deshalb keine Microservice-spezifischen Funktionen ab. Genau dort setzt ein Service Mesh an. Es handelt sich um eine zusätzliche Infrastrukturebene für das Management der Service-zu-Service-Kommunikation. Anders als ein ESB ist ein Service Mesh jedoch dezentral und implementiert keine Geschäftslogik, da es typischerweise kein Content-basiertes Routing umsetzt. Ähnlich wie Kubernetes beliebige Container orchestriert, ist der Zweck eines Service Mesh, beliebige Microservices zu verwalten.
Im Detail betrachtet besteht ein Service Mesh aus zwei zusätzlichen Ebenen, die zwischen einem Orchestrierer wie Kubernetes und den Anwendungen liegen. Ein Service Mesh stellt jeder Service-Instanz einen sogenannten Sidecar-Container zur Seite. In Kubernetes wird dieser in den gleichen Pod, der Deployment-Einheit von Kubernetes, platziert. Daher laufen beide Container auf demselben physischen Host, teilen sich ein Netzwerkinterface und können sich Dateisysteme teilen. In dem Sidecar-Container befindet sich ein Service Proxy, durch den jegliche eingehende und ausgehende Kommunikation verläuft. Alle Service Proxies zusammen bilden die dezentrale Data Plane (Datenebene), die nicht nur Werte wie Quelle und Ziel einer Anfrage, Latenz und Fehlercodes erfassen, sondern den Verkehr auch steuern und manipulieren kann. Die zweite Ebene, die ein Service Mesh hinzufügt, ist die Control Plane (Kontrollebene). Sie enthält zentrale Komponenten, die die Sidecar Proxies auf der Data Plane konfigurieren. Dazu gehört, wie die gesammelten Daten verarbeitet werden sollen, aber auch wie Netzwerkverkehr auf Basis von Regeln geprüft, geändert oder gelenkt werden soll.
Istio
Der bekannteste Vertreter eines Service Mesh ist Istio [6], was auf Griechisch segeln bedeutet und sich damit in die nautische Metaphorik der Kubernetes-Infrastruktur einreiht. Auch wenn Kubernetes offensichtlich die primär unterstützte Plattform ist, ist Istio grundsätzlich auch in anderen Umgebungen einsetzbar. Das von Google, IBM und Lyft initiierte Projekt ist open source und basiert wie Kubernetes konzeptionell auf einem System, das Google für die interne Infrastruktur entwickelt hat und schon lange verwendet. Istio nutzt neben Kubernetes weitere etablierte Technologien wie den Service Proxy Envoy, das Monitoringtool Prometheus, das Dashboard Grafana und das Tracing-Backend Jaeger. All diese Komponenten gehören standardmäßig zu einer Istio-Installation.
Die Data Plane von Istio besteht aus Instanzen von Envoy, einem Open Source Service Proxy, der von Lyft entwickelt wurde. Ein Kubernetes-Cluster kann durch Istio so konfiguriert werden, dass jeder Pod automatisch einen Sidecar-Container mit einem Envoy erhält. Wenn eine Anwendung eine Anfrage stellt, passiert diese die Envoy Proxies des Sender- und Empfänger-Pods, bevor sie den Ziel-Service erreicht. Auf diese Weise ermöglicht Istio einen konsistenten Einblick in eine Microservices-Anwendung und die lückenlose Kontrolle der Kommunikation zwischen Services (Abb. 1).
In der Control Plane wendet Pilot die Konfiguration auf die Envoy Proxies an. Bevor ein Envoy Proxy eine Anfrage an den Ziel-Service weitergibt, ruft er optional die Control Plane auf, die die Anfrage anhand von Regeln prüfen kann. Außerdem verarbeitet Mixer die durch die Envoy Proxies erfassten Daten und Citadel verwaltet die Zertifikate.
Monitoring
Kubernetes allein kann zumindest Systemmetriken anbieten wie zum Beispiel CPU- oder Speicherauslastung. Mit Istios Proxies ist es zusätzlich möglich, ohne Eingriffe in die Anwendung auch Metriken über die Requests zu erstellen: Anzahl, Durchsatz oder Latenzzeiten gehören dazu. Istio versteht nicht nur TCP sondern auch Protokolle wie HTTP/1.1, HTTP/2 oder gRPC. So kann Istio z. B. anhand des HTTP-Statuscodes auch entscheiden, ob ein Request erfolgreich bearbeitet worden ist oder nicht.
Die Metriken kann Istio auch gleich weiterverarbeiten und speichert sie in Prometheus [7]. Prometheus kann die Daten nach mehreren Dimensionen aufschlüsseln, also beispielsweise nach dem HTTP-Statuscode oder dem Container. So können die Metriken sehr einfach so ausgewertet werden, dass die Fehlerfrequenz über alle Microservices oder die Leistung eines einzelnen Microservices ermittelt werden kann. Prometheus bietet zwar auch eine Visualisierung, aber die ist eher einfach. Daher installiert Istio auch gleich Grafana [8], das sehr mächtige Visualisierungen bietet. Istio konfiguriert Grafana gleich so, dass es für jeden Microservice ein Dashboard gibt, das die wesentlichen Informationen darstellt (Abb. 2).
Die Metriken von Istio können Microservices nur als Blackbox betrachten: Durchsatz, Latenzen oder Fehler stellen das Verhalten der Microservices nach draußen dar. Wie sich die Microservices intern verhalten, bleibt offen; das kann jedoch in einer Microservices-Architektur ausreichend sein. Schließlich ist es wichtig, welche Fehler und Leistung beim Benutzer ankommen. Genau das kann Istio mit der Metrikunterstützung darstellen. Natürlich könnte man die Microservices so erweitern, dass sie auch interne Metriken an die Istio-Monitoring-Infrastruktur weitergeben. Aber schon durch das Deployment eines Microservice in die Istio-Infrastruktur entstehen aussagekräftige Metriken, sodass eine solche Erweiterung der Microservices optional ist.
Obwohl Istio in der Standardinstallation Gebrauch von etablierten Tools wie Prometheus und Grafana macht, ist es sehr lose daran gekoppelt. Eine Konfiguration anderer Systeme wie Datadog, Amazon CloudWatch [9], Google Stackdriver [10] und weiterer [11] ist möglich. Diese Infrastruktur-Backends werden mit einem Adapterpattern in der Istio-Mixer-Komponente angebunden. Sie empfängt alle Telemetriedaten und kann sie in die vom jeweiligen Infrastruktur-Backend benötigten Form umwandeln und zur Verfügung stellen.
Abb. 2: Monitoring- und Tracinginfrastruktur mit Prometheus, Grafana, Jaeger und Kiali
Tracing
Auch Microservices hängen voneinander ab; das erschwert die Fehlersuche: Wenn in einem Microservice ein Fehler auftaucht, kann es sein, dass ein anderer Microservice aufgerufen worden ist und dass dieser den Ursprung des Problems darstellt. Das gilt für fachliche Fehler und noch viel mehr für Performance: Wenn ein Microservice einen anderen Microservice aufruft, der sehr langsam ist, können so die Performanceziele gegebenenfalls nicht mehr erreicht werden.
Da alle Aufrufe zwischen Microservices durch die Istio-Infrastruktur laufen, kann Istio die Aufrufe verfolgen. Man spricht von Tracing. Aber wenn ein Microservice einen anderen aufruft, ist es nicht ohne Weiteres klar, welcher der eingehenden Aufrufe einen bestimmten ausgehenden Aufruf verursacht hat. Istio vergibt daher an jeden Request, der an das Microservices-System gestellt wird, eine Trace-ID als HTTP-Header. Diese Trace-ID muss als HTTP-Header bei jedem Aufruf weitergegeben werden. So ist klar, welcher eingehende Aufruf bei einem Microservice für welche ausgehenden Aufrufe verantwortlich ist. Das Weitergeben muss der Code der Microservices erledigen, sodass Tracing nicht ohne Modifikationen am Code umgesetzt werden kann (Abb. 3).
Abb. 3: Übertragen der Trace-ID
Die Informationen über jeden Aufruf wie die Trace-ID und die Dauer muss Istio an die Tracinginfrastruktur weitergeben. Auch hier installiert Istio gleich ein passendes Tool mit. Konkret ist es Jaeger [12], das den OpenTracing-Standard implementiert. Jaeger kann für einen Aufruf zeigen, wie viel Zeit in welchem aufgerufenen Microservice verbracht wird (Abb. 4). Außerdem kann Jaeger auch die Abhängigkeiten zwischen den Microservices auf Basis der Traces visualisieren.
Komplexe Aufrufbeziehungen verschlechtern die Performance des Systems, weil Aufrufe durch das Netzwerk langsam sind. Und bei jedem Aufruf muss man sich auch überlegen, was passiert, wenn der andere Service gerade nicht verfügbar ist. Tracing ist besonders nützlich, wenn die Microservices so geschnitten sind, dass komplexe Aufrufbeziehungen entstehen, was man eigentlich wegen der Performance und Ausfallsicherheit vermeiden sollte. Tracing kann auch nützlich sein, um Microservices zu identifizieren, die instabil oder nicht performant sind, auch wenn sie nur von anderen Microservices genutzt werden und die Probleme daher nicht offensichtlich sind.
Kiali [13] nutzt die Daten aus Jaeger, Prometheus und Grafana, um die Struktur des Systems mit den Abhängigkeiten zwischen den Microservices darzustellen. Außerdem kann Kiali die Istio-Konfiguration darstellen und die Workloads. Kiali ist speziell auf Istio abgestimmt, sodass es gut mit dem Service Mesh harmoniert.
Abb. 4: Beispieltrace in Jaeger
Software Architecture Summit vom 11. - 13. März in München
Technische und methodische Themen, Kommunikationstrends, Cloudlösungen, MLOps, Design und Psychologie
Logging
Kubernetes bietet ebenso Features für das Logging. Docker-Container können in die Standardausgabe loggen; Kubernetes bietet dann Zugriff auf diese Logs. Dazu kann zusätzlich noch eine Verarbeitung der Logs in der Kubernetes-Infrastruktur umgesetzt werden. Wenn ein Microservice nicht loggt, ist diese Infrastruktur allerdings sinnlos. Nur wenn alle Microservices einheitlich loggen, kann eine Infrastruktur die Logs einfach auswertbar und durchsuchbar machen. So sollte jeder Logeintrag einen Zeitstempel oder eine Priorität (Fehler, Info) enthalten. In der Praxis ist ein einheitliches Format oft eine Herausforderung, insbesondere wenn die Microservices mit unterschiedlichen Technologien und von verschiedenen Teams implementiert wurden.
Istio hat über Mixer Zugriff auf die Informationen aus den Proxies und kann zumindest diese Informationen durch einen Mixeradapter ähnlich wie bei den Metriken einem Logsystem zur Verfügung stellen. Dabei kann Istio natürlich ein einheitliches Logformat garantieren. So kann Istio zumindest einige Informationen loggen und das auch einheitlich, was gegenüber keinem Log oder keinem einheitlichen Log Vorteile bieten kann. Die Informationen sind dabei beispielsweise die Dauer der Requests, die HTTP-Statuscodes oder der aufgerufene URL. Das sind Informationen, wie sie auch in Webserverlogs gespeichert werden. Für die Auswertung der Logs installiert Istio keine weiteren Werkzeuge. Also muss man hier beispielsweise auf den ELK-Stack mit Elasticsearch für die Speicherung der Logs, Logstash für das Parsen und Kibana für das Visualisieren und Auswerten setzen.
Resilienz
Eine Herausforderung in einem Microservices-System ist die größere Anzahl an Servern und Prozessen sowie der Netzwerkverkehr. Dadurch ist die Wahrscheinlichkeit wesentlich höher, dass ein Teil des Systems ausfällt. Wenn der Ausfall eines Microservice dazu führt, dass ein anderer ausfällt, kann sehr leicht ein Dominoeffekt entstehen, bei dem immer mehr Microservices ausfallen, bis das gesamte System ausgefallen ist. Diese Kette muss durchbrochen werden: Der Ausfall eines Microservice darf also nicht dazu führen, dass andere Microservices ausfallen; man spricht von Resilienz, also Widerstandsfähigkeit.
Kubernetes allein kann ausgefallene Microservices neu starten oder beim Ausrollen einer neuen Version eines Microservice sicherstellen, dass die neue Version tatsächlich funktioniert und das System im Fehlerfall automatisch auf eine alte Version zurückfällt. Aber diese Maßnahmen erhöhen nur die Verfügbarkeit, wenn ein Neustart oder ein Zurückrollen hilft. Wenn der Microservice dauerhaft ausfällt, kann Kubernetes nichts mehr tun.
Istio hingegen kontrolliert den Netzwerkverkehr und kann daher mehr erreichen:
• Istio kann bei einem Aufruf einen Fehler erzeugen, auch wenn der aufgerufene Microservice eigentlich fehlerfrei funktioniert (Fault Injection). Das erscheint zunächst sinnlos, schließlich ist das Ziel ja gerade, solche Fehler zu vermeiden. Mit Fault Injection kann man ausprobieren, wie das System reagiert, ohne dass man dazu die Implementierung anpassen muss. Fehler können dabei durch HTTP-Fehlercodes oder lange Antwortzeiten simuliert werden.
• Istio kann auch einen Request wiederholen (Retry). Wenn der aufgerufene Microservice nur vorübergehend ausgefallen ist, kann so der Request vielleicht erfolgreich bearbeitet werden. Allerdings muss der aufgerufene Microservice damit zurechtkommen, dass die Anzahl der Requests durch die Retries größer wird.
• Zum Schutz vor solchen Problemen kann Istio ein Time-out implementieren; dann bricht der Proxy den Requests nach einer bestimmten Zeit ab. Dabei gibt es ein Zeitbudget für jeden einzelnen Retry und alle Retries zusammen. So kann der aufrufende Microservice davor geschützt werden, seine Threads zu lange zu blockieren. Wären alle Threads damit beschäftigt, auf die Antwort auf Requests an andere Microservices zu warten, würde der Microservice wahrscheinlich ausfallen.
• Um den aufgerufenen Microservice zu schützen, implementiert Istio einen Circuit Breaker (Sicherung). Wie eine Sicherung in einem Stromkreis unterbricht ein Circuit Breaker die Aufrufe an den Microservices. Istio kann so konfiguriert werden, dass nur eine bestimmte Anzahl Aufrufe parallel abgearbeitet werden dürfen und nur eine bestimmte Anzahl Aufrufe auf Bearbeitung warten dürfen. Außerdem können Microservices, die gerade Fehler zurückgeben, ganz von der Verarbeitung von Aufrufen ausgeschlossen werden.
Die Maßnahmen erhöhen zwar die Resilienz, aber nur Retries verringern die Anzahl von Fehlern bei Aufruf. Time-outs erzeugen Fehler anstatt dass der Request blockiert. Und Circuit Breaker erzeugen mehr Fehler, um den aufgerufenen Microservice zu schützen. Daher muss ein Microservice immer noch Logik implementieren, um mit solchen Fehlerzuständen umzugehen. Diese Logik kann fachlich sein. Ob man eine Bestellung annimmt, obwohl man die Lagerbestände gerade nicht überprüfen kann, ist eine Abwägung zwischen möglicherweise frustrierten Kunden, wenn Bestellungen angenommen werden und Waren doch nicht geliefert werden können, und entgangenem Umsatz, wenn Waren geliefert werden können, aber Bestellungen nicht angenommen werden.
Innerhalb der Istio Control Plane ist Pilot dafür zuständig, die Konfiguration für die Fault Injection, Time-outs, Retries und Circuit Breaker an die Proxies zu verteilen.
Routing
Microservices-Architekturen haben den großen Vorteil, dass Features innerhalb kürzester Zeit im Produktionssystem deployt werden können. Kubernetes bietet ein Rolling Update an, bei dem die Instanzen nach und nach ausgetauscht werden; der Prozess stoppt nur, wenn Fehler auftreten. Ein häufiges Pattern für das Releasen von kritischen Änderungen ist Canary Releasing. Neue Versionen werden für einen kleinen Teil der Nutzer freigeschaltet, sodass Auswirkungen ohne großes Risiko beobachtet und im Zweifel leichter zurückgenommen werden können. Mit Kubernetes ist Canary Releasing nicht ohne Weiteres umzusetzen. Mit Istio kann nicht nur die prozentuale Verteilung von Netzwerkverkehr (z. B. 1 % zur neuen Version, 99 % zur alten) konfiguriert werden, sondern z. B. auch die Verteilung anhand von HTTP-Headern (z. B. 30 % der User mit Firefox-Browser zur neuen Version und alle anderen zur alten).
Auch abgesehen von Canary Releasing können mit Mitteln von Istio komplexe Routingregeln formuliert und durchgesetzt werden. Diese Regeln werden zusammen mit der Netzwerktopologie, die Pilot von Kubernetes extrahiert, auf die Sidecar Proxies verteilt. Diese kennen damit für jeden Service alle potenziellen Endpunkte (bzw. die IP-Adressen der Pods) und können Routing, Load Balancing und Health Checking performant selbst durchführen.
Policies
Istio bietet die Durchsetzung verschiedener Policies an. Zum einen können, analog zum Monitoring, Infrastruktur-Backends über Mixeradapter angebunden werden, die Quotas oder White- bzw. Blacklists prüfen. Da diese Prüfungen über Mixer und externe Dienste durchgeführt werden, können sie das System jedoch verlangsamen. Policies, die ohne externe Backends durchgesetzt werden können, wie Autorisierungs- und Authentifizierungs-Policies, können deshalb allein durch die Envoy Proxies geprüft werden. Beispielsweise kann eine Autorisierungs-Policy formuliert werden, die Service A gestattet, mit GET auf /hello von Service B zuzugreifen, jedoch alle anderen Aufrufe verbietet [14], [15].
Für die Authentifizierung von Endnutzern können Policies formuliert werden, die die Gültigkeit von JWT [16] prüfen. Die Authentifizierung von Services ist in das Verschlüsselungskonzept integriert.
Security
Dieses Verschlüsselungskonzept nutzt ebenfalls die Proxies. In einem Istio Service Mesh wird jegliche Kommunikation zwischen Services über die Envoy Service Proxies abgewickelt. Da Istio damit beide Enden eines Anfragepfads kontrolliert, kann gegenseitiges Authentifizieren und Verschlüsselung mit mTLS zentral gesteuert werden. Die Control-Plane-Komponente Citadel erstellt, verteilt, entzieht und rotiert die dafür notwendigen Zertifikate. Durch die Überprüfung der Zertifikate kann die Identität eines Microservice festgestellt werden. Das erlaubt Authentifizierung der Microservices, d. h. es ist feststellbar, mit welchem Microservice man tatsächlich kommuniziert.
Fazit
Istio ist eine vielversprechende Technologie. Sie löst die meisten Herausforderungen in einem Microservices-System. Dazu sind noch nicht einmal größere Änderungen am Code der Microservices nötig. Das bedeutet auch, dass die Auswahl der Technologie für die Implementierung der Microservices durch Istio nicht eingeschränkt wird. Also ist Istio zusammen mit Kubernetes gut dazu geeignet, auch komplexe Microservices-Systeme zu implementieren.
Auf der anderen Seite ist Istio eine komplexe Technologie. Die Komplexität liegt aber gar nicht so in Istio begründet, sondern in den Herausforderungen, die bei Microservices-Systemen gelöst werden müssen und die Istio so komplex machen. So oder so bleibt eine steile Lernkurve.
Weil Istio bei jedem Aufruf aktiviert wird, schlägt Istio auf die Performance durch, ist aber auf eine möglichst geringe Latenz optimiert. Dennoch ist der Aufruf natürlich deutlich langsamer als wenn kein zusätzlicher Proxy und Service aufgerufen werden. Wie sehr das auf die Leistung des Systems durchschlägt, hängt davon ab, wie viel Arbeit in einem Aufruf bearbeitet wird. Der Overhead fällt bei jedem Aufruf an. Wenn es viele kurze Aufrufe gibt, wird der Overhead erheblich ins Gewicht fallen. Wenn es wenige Aufruf sind, die länger dauern, wird der Overhead nicht so sehr ins Gewicht fallen. Microservices, die viel mit anderen Microservices kommunizieren, haben aber sowieso einen hohen Overhead durch die vielen Aufrufe über das Netzwerk. Es ist außerdem schwierig, sie stabil zu bekommen, weil sie von der Verfügbarkeit und Performance vieler anderer Microservices abhängen. Istio verschlimmert in dem Fall also nur eine Situation, die es eh zu verhindern gilt.
Istio ist das Ergebnis der Erfahrung bei Google, die schon lange ein ähnliches System im Einsatz haben. Zumindest das Konzept hat sich also schon in einer IT-Infrastruktur bewährt, die eine der größten überhaupt ist. Google ist auch an der Implementierung von Istio beteiligt, sodass die Erfahrungen aus der Google-Infrastruktur direkt in die Implementierung von Istio einfließen.
Um sich in Istio einzuarbeiten, bieten sich die Istio Tasks [17] an. Sie zeigen schrittweise und ganz praktisch die verschiedenen Features von Istio, indem man sie anhand von Beispielen ausprobiert. Ebenso gibt es ein Beispiel [18] mit einer ausführlichen Anleitung, das anhand einer Microservices-Architektur zum praktischen Nachvollziehen der meisten Features einlädt. So kann man Istio direkt im Einsatz kennenlernen und ausprobieren.
Links & Literatur
_________________________________________________________________________________________________
[1] Wolff, Eberhard: „Microservices: Grundlagen flexibler Softwarearchitekturen“, 2. Auflage, dpunkt, 2018.
[2] Wolff, Eberhard: „Microservices – Ein Überblick“: https://microservices-buch.de/ueberblick.html
[3] Wolff, Eberhard: „Das Microservices-Praxisbuch: Grundlagen, Konzepte und Rezepte“, dpunkt, 2018
[4] Wolff, Eberhard: „Microservices Rezepte – Technologien im Überblick“: https://microservices-praxisbuch.de/rezepte.html
[5] https://kubernetes.io/
[6] https://istio.io/
[7] https://prometheus.io/
[8] https://grafana.org/
[9] https://aws.amazon.com/de/cloudwatch/
[10] https://cloud.google.com/stackdriver/
[11] https://istio.io/docs/reference/config/policy-and-telemetry/adapters/
[12] https://www.jaegertracing.io/
[13] https://www.kiali.io/
[14] https://istio.io/docs/reference/config/authorization/istio.rbac.v1alpha1/
[15] https://istio.io/docs/concepts/security/#authorization
[16] https://jwt.io
[17] https://istio.io/docs/tasks/
[18] https://github.com/ewolff/microservice-istio