O resilience, where art thou?

Service Mesh vs. Framework: Resilienz in verteilten Systemen mit Istio oder Hystrix

Michael Hofmann

© Shutterstock / dtosh

Je verteilter ein Software-System konzipiert wird, desto mehr sollte über Resilienz nachgedacht werden. Durch die Verteilung kann es immer wieder zu Fehlersituationen beim Aufruf der beteiligten Kommunikationspartner kommen. Um die Auswirkungen dieser Fehler möglichst gering zu halten oder eventuell ganz zu zu vermeiden, ist es mittlerweile üblich, mit den notwendigen Resilienz-Patterns zu arbeiten. Ob man sich besser via Service-Mesh-Werkzeug oder Framework um die Resilienz kümmert? Beide Ansätze haben etwas für sich.

Die meisten Probleme bei der verteilten Kommunikation kommen aus der Infrastruktur, wie zum Beispiel Abbrüche in der Netzwerkverbindung oder langsame Antwortzeiten bis hin zu Timeouts beim Aufruf eines anderes Services, um nur ein paar Beispiele zu nennen. Es existieren seit Jahren eine Menge Frameworks für jede gängige Programmiersprache, um diese Aufrufe abzusichern. Seit Kurzem entstehen allerdings auch sogenannte Service Mesh Tools, die den Einsatz von gängigen Resilienz-Patterns ermöglichen. Stellt sich also die Frage, warum man Resilienz durch Programmierung in den Services erreichen soll, wenn man die Probleme der Infrastruktur auch in der Infrastruktur selbst, nämlich im Service Mesh, beheben kann.

Notwendigkeit der Resilienz in verteilten Systemen

Die Notwendigkeit von resilienter Kommunikation in verteilten Systemen ist sicherlich nicht neu. Die bekannteste Sammlung von Fehleinschätzungen, die Liste der „Fallacies of Distributed Computing“, wurde schon um die Jahrtausendwende veröffentlicht. Doch jahrelang hat man sich in Projekten nicht sehr intensiv damit beschäftigt. Das lag unter anderem sicherlich daran, dass eher monolithische Systeme entstanden sind, bei denen diese Problemstellungen nicht an erste Stelle standen. Der neue Trend, Systeme für die Cloud zu entwicklen, also Cloud-native, rückt diese Infrastrukturprobleme wieder in den Fokus. Auch in den Projekten entsteht mehr und mehr das Mindset, sich diesen Problemen von Anfang an zu widmen.

Frameworks helfen

Eine Möglichkeit, Resilienz zu erreichen, ist der Einsatz von Frameworks. Die bekanntesten Vertreter im Java-Bereich sind zum Beispiel Hystrix, Resilience4J, Failsafe oder MicroProfile Fault Tolerance. All diese Frameworks bieten, mehr oder weniger, Hilfe bei der Umsetzung folgender Resilienz-Patterns an:

  • Timeout
  • Retry
  • Fallback
  • Circuit Breaker
  • Bulkhead

Bei Timeout, Retry, Circuit Breaker und Bulkhead muss der Entwickler im Grunde nur seine Aufruf-Methode annotieren oder entsprechend absichern und schon verläuft die Kommunikation fehlertoleranter. Hier ein Beispiel für einen Circuit Breaker implementiert mit MicroProfile Fault Tolerance:

@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 1000, successThreshold = 2)
public MyResult callServiceB() {
...
}

In einem rollierenden Fenster von 10 aufeinanderfolgenden Aufrufen (requestVolumeThreshold) wird ab einer Fehlerquote von 50% (failureRatio) der Circuit Breaker in den Status open versetzt. Nachfolgende Aufrufe werden dabei für mindestens 1000 Millisekunden (delay) unterbunden, bevor der Circuit Breaker in den Status half-open versetzt wird. Sollten in diesem Zustand zwei aufeinanderfolgende Aufrufe erfolgreich sein, so wird der Circuit Breaker wieder in den Anfangs-Zustand close versetzt.

Bei Fallback muss es dagegen möglich sein, einen alternativen Ablauf in der Business-Logik zu implementieren, sollte der eigentliche Aufruf fehlschlagen. Diese Möglichkeit ist nicht immer gegeben und kann auch nicht ohne der Existenz einer fachlichen Alternative umgesetzt werden. Von daher kann Fallback als ein fachlich motiviertes Resilienz-Pattern verstanden werden. Die vier anderen Patterns besitzen dagegen einen rein technisch Fokus. Wir kommen darauf zurück, doch zuvor wollen wir uns der Thematik der Resilienz in verteilten Systemen noch von einer anderen Seite her nähern.

Resilienz im Service Mesh

Ein Geflecht, das aus vielen verteilten Systemen besteht, die sich darüber hinaus auch noch gegenseitig aufrufen, wird als Service Mesh bezeichnet. Zur Verwaltung und Überwachung solcher Service Meshs existieren seit Kurzem geeignete Werkzeuge, wie zum Beispiel Istio oder Linkerd. Diese Tools können mittlerweile auf den gängigsten Cloud-Plattformen eingesetzt werden. Beiden Werkzeugen ist eine grundlegende Architekturentscheidung gemein, nämlich der Einsatz eines sogenannten Sidecars. Damit wird ein separater Prozess bezeichnet, der neben dem eigentlichen Service auf dem Zielknoten installiert wird. Im Falle von Kubernetes besteht ein Kubernetes Pod somit aus dem Service und einem zugehörigen Sidecar. Eine weitere wichtige Eigenschaft des Sidecars ist es, dass sämtliche Kommunikation von und zum Service durch den Sidecar-Prozess geleitet wird. Diese Umlenkung der Kommunikation ist für den Service völlig transparent.

Mit Hilfe des Sidecars, der die gesamte Kommunikation überwacht und steuert, ist es möglich, Fehler in der Kommunikation im Sidecar zu behandeln. Diesem Ansatz folgend bietet beispielsweise Istio auch mehrere Resilienz-Patterns an, die durch Istio-Regeln im Sidecar aktiviert werden können. Im Grunde sind dies:

  • Timeout
  • Retry
  • Circuit Breaker (mit Bulkhead-Funktionalität)

Im Vergleich zur Aufzählung der Resilienz-Patterns bei den Frameworks fehlt also auf den ersten Blick der Fallback und der Bulkhead. Da der Fallback, wie oben bereits ausgeführt, rein fachlich bestimmt ist, sollte klar sein, dass eine technische Komponente wie das Sidecar diese Funktionalität nicht anbieten kann. Doch was ist mit dem Bulkhead? Der Bulkhead limitiert die Anzahl an parallelen Aufrufen zu einem anderen Service. Damit wird der aufrufende Service davor geschützt, zu viele Threads für die parallelen Aufrufe zu verwenden und somit selbst in Schwierigkeiten, in Form eines vollgelaufenen Thread-Pools, zu geraten. Der Circuit Breaker bei Istio funktioniert so, dass er die Anzahl an gleichzeitig möglichen Aufrufen begrenzt. Steigt die Anzahl über den Schwellwert so unterbricht der Circuit Breaker im Sidecar den Aufruf und quittiert ihn mit einem Fehler. Selbstredend sollte der aufrufende Service mit einer entsprechenden Fehlermeldung auf die Unterbrechung (HTTP Status Code 503 – Service Unavailable) reagieren. Da der Aufruf vom Service durch das Sidecar zum aufgerufenen Service geleitet wird, wirkt der Circuit Breaker in diesem Fall wie ein Bulkhead, da er die Anzahl an parallelen Aufrufen im rufenden Service limitiert und somit indirekt auf den rufenden Service einwirkt.

Definition eines Circuit Breakers mit einer Istio-Regel:

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: mycircuitbreaker
spec:
  host: serviceb
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 10
      http:
        http1MaxPendingRequests: 1
        maxRequestsPerConnection: 1
    outlierDetection:
      consecutiveErrors: 3
      interval: 10s
      baseEjectionTime: 1m
      maxEjectionPercent: 100

Mit dieser Regel wird das Sidecar angewiesen, maximal 10 Verbindung (maxConnections) auf den Service serviceb (host: serviceb) zuzulassen. Darüber hinaus wird noch ein wartender HTTP-Request akzeptiert (http1MaxPendingRequests). Zusätzlich wird noch definiert, dass beim Auftreten von drei aufeinanderfolgenden Aufruf-Fehlern (consecutiveErrors), welche in einem Intervall (interval) von 10 Sekunden auftreten, 100% der Aufrufe (maxEjectionPercent) sofort für eine Minute (baseEjectionTime) unterbunden werden.

An dieser Stelle soll der Vollständigkeit halber noch darauf hingewiesen werden, dass die Service Mesh Tools noch sehr viel mehr an Funktionalität zur Verwaltung, Steuerung und Überwachung des Service Meshs bieten. Eine umfangreiche Beschreibung bietet unsere Artikelserie zu Istio.

Hat man sich im Projekt für einen Einsatz eines Service-Mesh-Werkzeugs entschieden, um zusätzliche nutzbringende Funktionalitäten zu verwenden, stellt sich folglich die Frage, ob die Umsetzung der Resilienz mittels Frameworks im Service oder mit Istio-Regeln im Sidecar erfolgen soll.

Frameworks vs. Service Mesh

Der naheliegende Gedanke eines Entwicklers, der sich um Resilienz kümmern muss, ist die Umsetzung mit einem Framework seiner Wahl. Der bekannteste Vertreter dieser Gattung ist Hystrix. Doch leider wird Hystrix seit Anfang 2019 nicht mehr weiterentwickelt und befindet sich seitdem im Wartungsmodus. Entwickler, die Hystrix bereits in Verwendung haben, müssen sich also früher oder später mit einer Migration auf ein anderes Framework beschäftigen. Zum Glück gibt es noch genügend Auswahl an Frameworks, die auch alle sehr gut ihren Dienst tun. Selbst bei einer Festlegung auf eine Programmiersprache (beispielsweise auf Java) kann man zwischen einer Reihe von verfügbaren Frameworks wählen.

Sofern die Entscheidung beim Entwickler-Team liegt, wird man sehr schnell eine Vielzahl von verschiedenen Frameworks, in unterschiedlichen Versionen und Verhaltensweisen im Bezug auf die Auslegung der Resilienz, erhalten. Sieht man sich die verschiedenen Frameworks im Detail an, dann gibt es durchaus Unterschiede in der Umsetzung der einzelnen Resilienz-Patterns. Darüber hinaus kann im Laufe eines Projekts das gleiche Framework in unterschiedlichen Versionen in den einzelnen Services zeitgleich eingesetzt sein. Auch hierbei kann es zwischen den einzelnen Versionen desselben Frameworks zu leicht unterschiedlichem Verhalten kommen. Somit hat jedes Entwickler-Team seine eigene, spezielle und auf das Framework bezogene Lernkurve.

Die Migration im Service auf ein alternatives Resilienz-Framework ist mit mehr Aufwand verbunden als der vergleichbare Wechsel von einem Service Mesh Tool zu einem anderen. Im Falle der Framework-Migration müssen in jedem Service alle Code-Stellen neu annotiert und anschliessend der komplette CI-CD-Lauf neu gestartet werden. Darüber hinaus ist ein Deployment des neuen Services notwendig, was bei der Variante mit der Umsetzung über Sidecar-Regeln unterbleiben kann. Im Vergleich zum Umschreiben der Resilienz-Regeln kann hierbei also mehr Aufwand entstehen.

Fällt die Entscheidung beispielsweise auf Hystrix, so liegt die Versuchung nahe, dass man für den Einsatz des Client-seitigen Load Balancing gleich noch Ribbon verwendet und sich für die passende Service-Registry in Form von Eureka entscheidet. Das ergibt durchaus Sinn, da diese Komponenten gut aufeinander eingestimmt sind. Doch Projekte, bei denen man sich gegen den Einsatz von Hystrix entschieden hat, könnten spätestens bei der Diskussion um die passende Service-Registry eine andere Präferenz haben. In einem Punkt sind sich wohl beide Parteien einig: der Einsatz von zwei Service-Registrys ist sicherlich nicht erstrebenswert.

API Confernce 2019
 Maik Schöneich

gRPC – Ein neuer heiliger Gral?

mit Maik Schöneich

Oliver Drotbohm

REST Beyond the Obvious – API Design for Ever Evolving Systems

mit Oliver Drotbohm (Pivotal Software, Inc.)

 

IT Security Summit 2019
Arne Blankerts

Sichere Logins sind doch ganz einfach!

mit Arne Blankerts (thePHP.cc)

Marcus Bointon

Hands-on workshop – Hansel & Gretel do TLS

mit Marcus Bointon (Synchromedia Limited)

Die Sidecars im Service Mesh betreiben auch Clien-seitiges Load Balancing, da das Sidecar selbst für das Routing sorgt. Als Service-Registry dienen die Deployment-Informationen, die von der Cloud-Plattform ausgelesen und für das Routing entsprechend umgesetzt werden.

Der Grund für die Abkündigung von Hystrix wird auf der GitHub-Seite auch gleich dargelegt: Netflix hat erkannt, dass es schwierig ist, mit Konfigurationen der Resilienz-Patterns zeitnah auf Laufzeitschwankungen zu reagieren. Bei dem neuen Ansatz wird versucht, mit adaptiven Implementierungen schneller darauf reagieren zu können. Bis dieser Ansatz erfolgreich und somit verwendbar ist, wird wohl allerdings noch einige Zeit vergehen.

Die Konsequenz, die hinter dieser Grundaussage steckt, ist, dass eine Anpassung der Resilienz-Einstellungen in den einzelnen Services sehr einfach und rasch möglich sein muss. Je nachdem wie gut man die Konfiguration seiner Services ermöglicht hat (Config-Files, Config-Maps, Config-Server), gestaltet sich die Umsetzung in der Framework-Variante mehr oder weniger aufwendig. Auch bei den flexibelsten Ansätzen kann es notwendig werden, die Services neu zu starten, damit die neuen Einstellungen greifen.

Im Falle einer neuen oder veränderten Resilienz-Regel muss diese einfach nur ausgeführt werden (kubectl apply -f ...) und alle Sidecars bekommen, nach einer kurzen Verzögerung, die neuen Werte für die Umsetzung der Regel. Die anschließende Einhaltung der neuen Regel kann ohne einen Neustart des Sidecars oder des Services erfolgen. Der Service selbst merkt davon nichts.

Die Gefahr des Retry Storms

Oft ist es so, dass man das am besten geeignete Resilienz-Pattern erst nach ein paar (Fehl-)Versuchen ermittelt. Gerade am Anfang ist die Lernkurve in diesem Gebiet sehr steil. Auch erfahrene Architekten oder Entwickler tun sich schwer, auf Anhieb den richtigen Ansatz für die Resilienz zu finden. Das liegt zum Teil an sehr komplexen Kommunikationsabläufen, die noch dazu starken Laufzeitschwankungen unterliegen. Gesteigert wird das Ganze noch durch unterschiedlichste Probleme, die von beteiligten Infrastrukturkomponenten ausgelöst werden können. Sogar ein einfaches Pattern wie Retry kann zu Problemen führen. Durch den mehrfachen Versuch, den fehlerhaften Aufruf zu kompensieren, kann es bei einem problematischen Service zu einer Flut von Aufrufen kommen, welche unter Umständen zur weiteren Verschlechterung der Service-Verfügbarkeit führt. Im Grunde wird damit die Situation nur noch verschlechtert. Man spricht hierbei von einem sogenannten Retry Storm.

In einer solchen Situation wünscht man sich einen flexiblen Wechsel des eingesetzten Resilienz-Patterns und nicht nur eine Anpassungsmöglichkeit der Einstellungswerte. Im Falle der Programmierung muss der Entwickler wieder tätig werden. Der Einsatz von Regeln bietet hier die weitaus flexiblere Möglichkeit, da dies abermals nur durch eine neue Regelausführung erfolgen kann. Sollte der gewünschte Effekt ausbleiben, kann man auch sehr schnell wieder die alte Regel aktivieren. Der Try-and-error-Mechanismus kann somit viel einfacher und schneller vonstattengehen.

Der Sinn und Zweck beim Einsatz eines Circuit Breakers liegt auch im Schutz des aufgerufenen Services. Mit dem Zustand half-open oder open soll erreicht werden, dass der aufgerufene Service sich unter Umständen wieder von Hochlastphasen erholen kann. Die Erholung des Services ist um so schwieriger, je mehr Anfragen in dieser Zeit eintreffen. Oft führt diese Überlast zum Absturz des Services. Von daher ist es essentiell, dass alle Aufrufer diesem Service eine Erholungsphase gönnen. Ein einziger Aufrufer, der sich nicht daran hält, kann den aufgerufenen Service trotzdem zum Absturz bringen.

Im Framework-Fall muss jeder Service mit dem Circuit Breaker ausgestattet werden. Ein Entwickler-Team, das sich nicht daran hält, kann schon Laufzeitprobleme verursachen. Im Gegensatz hierzu wirkt eine Istio-Regel auf alle Aufrufer des Services. Ein schwarzes Schaf kann sich somit gar nicht unter die Herde mischen.

Visualisierung

Was helfen die ganzen Überlegungen zu den Resilienz-Patterns, wenn gar nicht oder nur schwer festzustellen ist, wie sich das einzelne Pattern gerade unter Last oder in Fehlersituationen verhält? Genau aus diesem Grund gibt es bei Hystrix ein eigenes Dashboard, mit dem man sich die Zustände der eingesetzten Resilienz-Patterns graphisch darstellen lassen kann. Bei MicroProfile Fault Tolerance ist es möglich, diese Zustandsdaten über Metriken im Prometheus-Format ausgeben zu lassen. Wird nun ein Prometheus-Server, am besten zusammen mit Grafana, eingesetzt, so können diese Metriken im Grafana Dashboard graphisch dargestellt werden.

Denselben Ansatz verfolgt Istio. In der Standard-Installation sind der Prometheus- und der Grafana-Server mit von der Partie. Sämtliche Metriken des gesamten Service Meshs werden out of the box gesammelt und an den Prometheus-Server geschickt. Über unterschiedlich vordefinierte Dashboards kann man dem gesamten Service Mesh auf den Zahn fühlen. Das gilt selbstverständlich auch für die Metriken, welche die Services auf Basis der Resilienz-Patterns erzeugen.

Grafana-Dashboard / Quelle: istio.io

Fehler bewusst verursachen

Als weiteren Pluspunkt für Istio muss noch die Möglichkeit der Fault Injection erwähnt werden. Mit speziellen Regeln kann bei Istio ein Fehlverhalten, welches vom Sidecar ausgeführt wird, erzeugt werden:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: serviceb_failure
  ..<.
spec:
  hosts:
  - serviceb
  http:
  - fault:
    delay:
      fixedDelay: 10s
      percent: 90
    match:
    - headers:
      end-user:
        exact: user01
    route:
    - destination:
      host: serviceb
      subset: v1
  - route:
    - destination:
      host: serviceb
    subset: v1

Mit dieser Regel wird beim Aufruf von serviceb bei 90% (percent) der Aufrufe eine feste Verzögerung (fixedDelay) von 10 Sekunden erzeugt. Das Ganze tritt nur dann in Kraft, wenn im HTTP-Header (headers) ein Feld mit Namen end-user enthalten ist, das mit dem Wert user01 belegt ist. Alle anderen Requests werden normal weitergeleitet. Auf diese Art und Weise kann man das Verhalten des Service Meshs im Falle von verzögerten Antwortzeiten sehr einfach testen. Neben Timeouts ist es auch noch möglich, mit kompletten Abbrüchen Fehler in den Requests zu simulieren. In diesem Fall unterbricht das Sidecar den Aufruf und meldet einen passenden HTTP-Status-Code oder einen TCP-Connection-Fehler zurück. Eine entsprechende Fehler-Simulation kann mit dem Framework-Ansatz nur sehr schwer erzeugt werden.

Wer die Wahl hat...

Wie so oft im Leben gibt es nicht nur Schwarz oder Weiß. Je nach Umfeld oder Gegebenheiten muss man sich für den einen oder anderen Grauton zwischen den beiden Extremen entscheiden. Der fehlende Fallback bei Istio kann nur durch Einsatz eines Frameworks behoben werden. Für alle anderen Resilienz-Patterns bietet Istio mit seinem Regelwerk entsprechend gute Alternativen zum Framework-Einsatz. Auch die Möglichkeiten der Fehlersimulation mit Istio sind mit Frameworks nicht so einfach nachzustellen.

Der Einsatz eines Service-Mesh-Werkzeugs ist sicherlich mit einer hohen Komplexität und gesteigerten Aufwand verbunden. Doch dafür erhält man nicht nur die Resilienz-Patterns, sondern auch noch eine ganze Menge mehr an Funktionalität, die für das Verwalten und Beherrschen des Service Meshs essentiell sind. Ein Blick in die Funktionskiste von Istio oder Linkerd sollte sich jedes Projekt einmal gönnen.

Bis der neue Ansatz von Hystrix verwendbar ist, muss man sich wohl noch ein wenig gedulden und versuchen, mit herkömmlichen Mitteln seine Infrastruktur in den Griff zu bekommen.

Der Entwickler oder Architekt, der für den Einsatz von Resilienz in einer ersten natürlichen Reaktion in die Framework-Kiste greift, sollte kurz innehalten und überlegen, ob dies in seinem Fall die beste Alternative darstellt. Denn eigentlich sollte man die Infrastruktur-Probleme dort lösen, wo sie entstehen, nämlich in der Infrastruktur selbst!

Geschrieben von
Michael Hofmann
Michael Hofmann
Michael Hofmann ist freiberuflich als Berater, Coach, Referent und Autor tätig. Seine langjährigen Projekterfahrungen in den Bereichen Softwarearchitektur, Java Enterprise und DevOps hat er im deutschen und internationalen Umfeld gesammelt. Mail: info@hofmann-itconsulting.de
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: