Wozu Hystrix?
Hystrix ist ein von Netflix entwickeltes auf Github bereitgestelltes quelloffenes Framework zur Kapselung, Entkopplung und zum Überwachen von Abhängigkeiten. Hystrix erleichtert das Implementieren von zuverlässigen Komponenten in verteilten Umgebungen, in denen der Ausfall von einzelnen Bestandteilen nicht vermieden werden kann.
Hystrix verwendet das Command-Muster, um potenziell fehleranfällige Anfragen zu kapseln. Das Framework selbst bietet die Möglichkeit, eigene Commands synchron oder asynchron auszuführen und zur Laufzeit die Häufigkeit, Fehlerraten und Laufzeiten zu messen.
Jede Abhängigkeit wird dabei mit einem Kommando gekapselt und dann ausgeführt. Das Framework ist damit in der Lage, Aufrufe zu poolen, zu sammeln, Fehler zu protokollieren, Statistiken zu erstellen und bei Bedarf sogenannte Fallback-Methoden aufzurufen.
Wie schnell die Verfügbarkeit einer Komponente sinkt, wenn sie von mehreren anderen abhängt, zeigt Tabelle 1.
- || Anzahl an Abhängigkeiten ||||||||
- :—: | :—-: | :—-: | :—-: | :—-: | :—-: | :—-: | :—-: | :—-: | :—-: | 99,99% | 99,99% | 99,98% | 99,97% | 99,95% | 99,93% | 99,90% | 99,88% | 99,86% | 99,85% | 99,90% | 99,90% | 99,80% | 99,70% | 99,50% | 99,30% | 99,00% | 98,81% | 98,61% | 98,51% | 99,00% | 99,00% | 98,01% | 97,03% | 95,10% | 93,21% | 90,44% | 88,64% | 86,87% | 86,01% | 98,00% | 98,00% | 96,04% | 94,12% | 90,39% | 86,81% | 81,71% | 78,47% | 75,36% | 73,86% | 97,00% | 97,00% | 94,09% | 91,27% | 85,87% | 80,80% | 73,74% | 69,38% | 65,28% | 63,33% | 96,00% | 96,00% | 92,16% | 88,47% | 81,54% | 75,14% | 66,48% | 61,27% | 56,47% | 54,21% | 95,00% | 95,00% | 90,25% | 85,74% | 77,38% | 69,83% | 59,87% | 54,04% | 48,77% | 46,33% | 90,00% | 90,00% | 81,00% | 72,90% | 59,05% | 47,83% | 34,87% | 28,24% | 22,88% | 20,59% | [Tabelle 1: Übersicht über die theoretische Verfügbarkeit bei einer oder mehreren Abhängigkeiten zu Komponenten mit bezeichneter Verfügbarkeit]
Hystrix’ Design-Prinzipien
Das Hystrix Wiki [1] beschreibt die dem Entwurf von Hystrix zugrunde liegenden Prinzipien. In erster Linie darf keine Abhängigkeit alle verfügbaren Nutzer-Threads (des Containers) aufbrauchen können. Daher ist es besser, Requests zu verlieren und Fehler zu melden, als Anfragen zu queuen.
Wenn möglich, sollen Anwendern beziehungsweise Clients Fallbacks geliefert werden, anstatt eine Anfrage nicht beantworten zu können und zu scheitern. Hystrix verwendet Isolationsmechanismen (wie Bulkhead, Circuit Breaker und Timeouts), um die potenziellen Auswirkungen jeder einzelnen Abhängigkeit zu begrenzen. Quasi jeder Aspekt und jedes Verhalten von Hystrix kann durch Konfiguration angepasst werden.
Hystrix ermöglicht kurze Feedback-Zyklen zur Laufzeit. Durch kontinuierliche zeitnahe Messungen, Monitoring und Alarme kann das Laufzeitverhalten des Systems quasi live mitverfolgt werden. Durch das dynamische Konfigurationssystem werden Konfigurationsänderungen zur Laufzeit kaum verzögert propagiert. Dadurch lässt sich das System unmittelbar auf sich ändernde Lastszenarien anpassen und die benötigte Zeit zur Wiederherstellung der vollen Funktionsfähigkeit gering halten.
Metriken integriert
Für die Messung von Client-Aufrufen lassen sich Metriken konfigurieren. Hystrix misst zur Laufzeit verschiedenste Daten wie zum Beispiel Anzahl der erfolgreichen Aufrufe, der Fehler, der Cache-Zugriffe, der zusammengefassten Aufrufe und vieles mehr. Der Kasten „Kurzübersicht Metrics” liefert einen Überblick über die bereitgestellten Messinstrumente.
Damit Hystrix weiß, welche Messwerte in unserem System gesammelt werden sollen,
muss Hystrix entsprechend konfiguriert werden. Dazu verwendet Hystrix ein
hierarchisches Namensschema für Konfigurationsparameter: Das Präfix für die
Konfiguration eines Kommandos ist stets hystrix.command.<NAME DES KOMMANDOS>
.
Um den Default für alle Kommandos zu setzen, kann man für NAME DES KOMMANDOS
auch default
einsetzen. Um Metriken für ein Kommando zu konfigurieren, fügt
man an das Präfix den entsprechenden Konfigurationsparameter an.
Kurzübersicht Metrics
Metrics bietet eine Registry, über die verschiedene Messinstrumente registriert werden können, um von unterschiedlichen Programmteilen einfach verwendet werden zu können. Zum Messen von Daten bietet Metrics
- Gauges – Messinstrumente, die das aktuelle Messergebnis wiedergeben,
- Counters – Zähler, also Messinstrumente für ganzzahlige Werte,
- Meters – messen die Quote von Ereignissen pro Sekunde, pro Minute, pro 5-Minuten, pro 15-Minuten,
- Histograms – Histogramme, liefern die statistische Wert-Verteilung, Minimum, Maximum, Durchschnitt, Mittelwert, 75%-, 90%-, 95%-, 98%, 99%- und 99,9%-Perzentil,
- Timers – messen die Häufigkeit und Dauer von Aufrufen.
Die erfassten Messwerte können über verschiedene Back-Ends dann aufbereitet und auch visualisiert werden. In Hystrix integriert sind derzeit Implementierungen für Yammer und Servo.
Möchten wir beispielsweise Perzentile messen, können wir über den
ConfigurationManager
die Anzahl der Buckets für den Konfigurationsparameter
hystrix.command.default.metrics.rollingPercentile.numBuckets
setzen. Dies
geschieht zum Beispiel in einer Initialisierungsroutine so
Die erfassten Messwerte können in einem Hystrix-eigenen Dashboard beobachtet werden.
Das Szenario
In der vergangenen Ausgabe haben wir ein fiktives Rating-System [2] skizziert. So ein Bewertungssystem fragt innerhalb eines Rating-Vorgangs auch andere Auskunftsdienste ab, um ein Gesamtergebnis zu berechnen. Abbildung 1 zeigt das System Rating-Core mit seinen Um-Systemen – einschließlich der beiden Abhängigkeiten zu dem Schufa-Connector und dem Document-Archive. Konzentrieren wir uns erst einmal auf das Document-Archive.
Das Document-Archive aufrufen
Die Implementierung eines Aufrufs des Dokumentenarchivs ist einfach. Über ein einfaches GET kann ein Dokument geladen werden. Über POST können Dokumente abgelegt werden. Jede dieser Interaktionen kapselt man in Hystrix mit einer Implementierung des Kommando-Musters, das wir in seiner Grundform aus [3] kennen.
Wir müssen also ein Kommando zum Laden eines Dokuments (FetchDocumentCommand) und eines zum Ablegen eines Dokuments (StoreDocumentCommand) implementieren. Nehmen wir das Beispiel „Laden eines Dokuments“.
Anstatt die Abfrage des Archivsystems im Rating-Core so direkt zu implementieren
kapseln wir den Aufruf in einer speziellen von Hystrix-Command abgeleiteten Implementierung. Die für den Aufruf benötigten Parameter werden in Attributen unseres Kommandos zwischengespeichert und vom Konstruktor initialisiert.
Die Methode run()
in Listing 1 enthält den eigentlichen Client-Code. Diese
Methode rufen wir aber nicht selbst auf, sondern überlassen das dem Hystrix-Framework.
Hystrix-Kommandos aufrufen
Damit ein Kommando ausgeführt werden kann, müssen wir es Hystrix zur Ausführung geben. Dazu stehen drei verschiedene Methoden zur Verfügung:
-
execute()
für den synchronen Aufruf, -
queue()
für den asynchronen und -
observe()
, worauf wir später noch eingehen.
Das gewünschte Verhalten der obigen Zeile können wir nun mit dem Kommando einfach schreiben
Der execute()
-Aufruf lässt nun Hystrix zaubern.
Sicherungsschotts und Leistungsschutzschalter
In [4] hat Michael Nygard ein Muster für die Stabilität des Systems beschrieben: das Bulkhead-Muster. Er schreibt dazu
Protecting critical clients by giving them their own pool to call.
Abhängig von dem Abstraktionsgrad kann mit dem Pool ein Rechenzentrum, eine Server-Farm, (virtuelle) Server, Applikationen, Thread-Pools oder auch Connection-Pools gemeint sein.
Das im Zusammenhang mit seinem Buch deutlich häufiger beschriebene Muster ist das Circuit Breaker-Muster. Es dient dazu, eine Aufrufkette zu unterbrechen, sobald ein Fehler festgestellt wird. So wird verhindert, dass Aufrufe eines defekten Systemteils den aufrufenden Client in Mitleidenschaft ziehen und so nach und nach das Gesamtsystem lahmlegen.
Abbildung 2 skizziert den Zustandsautomaten, der dem Circuit Breaker-Muster zugrunde liegt. Er zeigt, wie die Aufrufkette unterbrochen wird, wenn die Fehlerhäufigkeit bei den Aufrufen einen definierten Schwellwert erreicht.
Solange der Leistungsschutzschalter geschlossen ist, werden die Aufrufe durchgelassen. Jeder Fehler wird gezählt, bis der Schwellenwert erreicht ist und der Leistungsschutzschalter die Aufrufkette unterbricht. Einmal unterbrochen werden Aufrufe erst nach einer gewissen Zeit (dem Timeout) wieder durchgelassen. Ein erfolgreicher Aufruf setzt den Fehlerzähler zurück und alles ist wieder gut.
Auf Fehler reagieren
Wenn im System einzelne Komponenten oder Services nicht zur Verfügung stehen, sollten wir darauf angemessen reagieren. Wie oft haben wir schon Kaskaden von Exception-Handlern geschrieben, die unsere schöne Fachlogik umlagerten? Auch wenn man sie seit Java 7 dank Multi-Catch abkürzen kann, möchte man doch eigentlich keine catch-Klauseln schreiben.
Hystrix bietet dafür Framework-seitig das Konzept des Fallbacks. Analog zur in
Listing 1 dargestellten Methode run()
können wir in unserem Hystrix-Command
eine Methode getFallback()
definieren, die im Falle einer Exception oder
eines Timeouts dann das Ergebnis im Fehlerfalle zurückliefert.
Listing 2 zeigt die Methode, die im Fehlerfalle aufgerufen werden soll. In unserem Beispiel geben wir einfach null zurück. Hier könnte man sich auch den Aufruf eines Backup-Connectors vorstellen, von dem die Dokumente geladen werden sollten.
Gleich, welche Funktionalität benötigt wird: Wir können sie einfach als Fallback implementieren und brauchen dafür keinen try-catch-Block.
Abbildung 3 zeigt schematisch, wie Hystrix die Stabilitätsmuster Bulkhead und Circuit Breaker einsetzt und wie es das Monitoring integriert.
RXJava geht auch
Wenn wir über neue Daten informiert werden wollen, können wir auf den Kommandos
anstelle der Methode execute()
die Methode observe()
aufrufen, das ein
Observable zurück liefert. Hystrix verwendet für das Reactive Programming, das
bereits in dieser Kolumne vorgestellte RXJava [5].
Zur Erinnerung: Bei RXJava können Beobachter an sogenannten Observables
registriert werden, die vom Observable für jedes Element durch den Aufruf von
onNext()
des Observers benachrichtigt werden. Ist ein Observable am Ende des
Datenstroms angelangt, benachrichtigt es die registrierten Beobachter über
deren Methode onComplete()
. Fehler werden per Aufruf der Beobachter-Methode
onError()
propagiert.
Hystrix-Kommandos können per toObservable()
oder auch observe()
in RXJava-Observables konvertiert werden.
Gruppieren von Kommandos
Eingangs haben wir bereits die Konfigurierbarkeit von Hystrix erwähnt. Abhängigkeiten können je nach Bedarf mit unterschiedlichen Strategien vom Rest des Systems isoliert werden. Die hierarchische Konfiguration ermöglicht beispielsweise das detaillierte Protokollieren von externen Abhängigkeiten einerseits und das grobgranulare Protokollieren andererseits.
Darüber hinaus können die Isolationsstrategien und Thread-Pools für eine Gruppe von Kommandos individuell gesetzt werden. Jedes Kommando definiert seine Gruppenzugehörigkeit beim Instanziieren über den Super-Aufruf im Konstruktor. In Listing 1 kann man in der ersten Zeile des Konstruktors sehen, dass FetchDocumentCommand zur Gruppe „DocArchive“ gehört.
Threads und Semaphore
Hystrix bietet zwei Isolationsstrategien an, mit denen die Ausführung eines
Kommandos isoliert werden kann: das Entkoppeln eines Aufrufs in einem eigenen
Thread sowie das Beschränken der nebenläufigen Ausführung von Aufrufen mit
Hilfe von Semaphoren. Hystrix verwendet standardmäßig eigene Threads für den
Aufruf der run()
-Methode. Das kostet zwar ein wenig Overhead, erlaubt aber
das Setzen von Timeouts und das frühzeitige Fortsetzen der restlichen
Verarbeitung. Die beiden Isolationsstrategien können die Anzahl der parallel
ausführbaren Aufrufe begrenzen.
Wollten wir beispielsweise sicherstellen, dass ein Kommando nicht wie üblich im separaten Thread ausgeführt wird, könnten wir dafür als Strategie Semaphore konfigurieren. Dadurch wird der Overhead eingespart, den die Verwendung von Threads mit sich bringt. Allerdings dient ein Semaphor nur dazu, die Anzahl paralleler Aufrufe zu begrenzen. Die Fähigkeit Service-Aufrufe mit Timeouts zu versehen, bieten Semaphore nicht. Dazu ist dann doch ein separater Thread nötig.
Im Rating-System bietet sich zum Beispiel das Begrenzen der parallelen Aufbereitung von Reports an, da deren Erstellung recht viel Speicher benötigt. Daher würden wir dafür in der Initialisierungsroutine konfigurieren
Optimierte Client-Aufrufe
Das Command-Muster erlaubt Hystrix nicht nur die Isolation von Client-Abhängigkeiten, sondern ermöglicht auch das Sammeln von Client-Aufrufen, um die zugehörigen Service-Aufrufe en block auszuführen.
Dazu können in Hystrix sogenannte Collapser konfiguriert werden. Am Beispiel des Document-Archives könnte ich mir vorstellen, dass man nicht jedes Dokument einzeln vom Archiv laden möchte, wenn in einem Rating-Vorgang stets alle benötigt werden. Dafür würden wir dann also konfigurieren
Mit dieser Konfiguration gibt Hystrix unsere Aufrufe von execute()
oder
queue()
nicht direkt an das Document-Archive weiter, sondern sammelt sie, bis
das Batch-Limit (maxRequestsInBatch
) erreicht ist oder der Timer wieder die
noch nicht verarbeiteten Requests im Batch ans Document-Archive weiter gibt.
Das Sequenz-Diagramm in Abbildung 4 zeigt das Szenario, in dem ein Client drei
Dokumente vom Archiv holen möchte und sie im Batch abgeholt werden.
Analog zum Zusammenlegen von Requests kann Hystrix auch so konfiguriert werden, dass Anfrage-Ergebnisse innerhalb eines Request-Zyklus gecached werden können.
Fazit
Hystrix ist ein tolles Framework zum Realisieren von verteilten Systemen. Dank der integrierten Isolationsmuster kann die Reaktionsfähigkeit des Systems auch bei partiellen Ausfällen aufrechterhalten werden. Dank der Integration von Metriken samt Dashboard zum live Überwachen des Systems während des Betriebs und des Konfigurationsframeworks, das die Anpassung an bestehende Rahmenparameter ebenfalls während des Betriebs erlaubt, stehen die Chancen gut, das System zur Laufzeit anzupassen und lange Ausfallzeiten zu vermeiden.
Ein weiterer Vorteil ist das geradlinige Programmiermodell: Jede Art von Interaktion mit einem zuliefernden System wird in einem Kommando gekapselt. Optimiert wird unter der Haube. Hystrix Request Collapser erlauben es, fachlich sinnvolle Client-Modelle zu bauen und nachträglich die Optimierung mit Batch-Requests zu konfigurieren.
Quellen, Links und Interessantes
Wikipedia-Eintrag über Graphite: http://de.wikipedia.org/wiki/Graphite
Github-Seite von Hystrix: https://github.com/Netflix/Hystrix
Java VM implementation of Reactive Extensions: https://github.com/Netflix/RxJava
Referenzen
-
Ph. Ghadir, Ph. Schirmacher, Domain–driven Design in Clojure, in: JavaSPEKTRUM, 2/2014 ↩
-
E. Gamma, R. Helm, R. Johnson, J. Vlissides, Entwurfsmuster. Elemente wiederverwendbarer objektorientierter Software, Addison–Wesley, 1996 ↩
-
M. Nygard, Release It!, Pragmatic Bookshelf, 2007 ↩
-
Ph. Ghadir, Reactive Extensions in Java, in: JavaSPEKTRUM, 5/2013 ↩