Resilience in der Praxis – Spring Cloud Netflix
In den letzten Jahren haben wir viel über moderne Microservice-Architekturen gelesen und gehört: sie wurden uns als das Wunderheilmittel verkauft. Doch was kam dabei raus, nutzen und bauen wir nun alle Services wie sie in den Lehrbüchern stehen und uns auf Konferenzen angepriesen werden? Ich bin davon überzeugt, die Antwort lautet: Nein! Wir konnten viele Abhängigkeiten während der Entwicklung auflösen und deutlich die Time-to-Market verkürzen. Nicht jedes Unternehmen hat die notwendigen Ressourcen, ihre über Jahre oder gar über Jahrzehnte gewachsenen Monolithen durch moderne Microservices zu ersetzen. Das ein oder andere Cobol-Programm verrichtet auch heute noch seine Dienste und jeder hofft, dass es dies auch in Zukunft noch ohne Probleme schafft, auch wenn dafür der alte Fritz das ein oder andere Mal aus seinem Ruhestand geholt werden muss.
Im nachfolgenden Artikel möchte ich auf Spring Cloud Netflix eingehen und aufzeigen, wie Spring Cloud Netflix dabei helfen kann, moderne Microservice-Architekturen aufzubauen bzw. wie es uns hilft, die Anbindung von alten Legacy-Systemen abzusichern. Um das ganze besser zu verdeutlichen, habe ich hierfür eine Demo gebaut und auf GitHub [1] bereitgestellt.
Resilience
Immer wieder hört man in der Presse und auf Konferenzen vom Thema Resilience und wie unverzichtbar dies in modernen Microservice-Landschaften geworden ist [2].
Vereinfacht gesprochen ist mit Resilience die Erstellung von robuster und fehlertoleranter Software gemeint. Beim ersten Blick in das Thema Resilient Software Design fühlt man sich mehr als erschlagen, eine Unzahl an möglichen Mustern tritt zu Tage, die erst mal verstanden und einsortiert werden müssen. Im Artikel werde ich daher nur auf ein paar der Muster eingehen, die durch Spring Cloud Netflix umgesetzt werden können.
In Anlehnung an die Ausarbeitung meines Kollegen Uwe Friedrichsen [1], habe ich die durch Spring Cloud Netflix Stack erreichbaren Pattern in der Abbildung dargestellt. Gehen wir diese nun nach und nach durch und erfahren welche Komponente aus dem Spring Cloud Netflix Stack uns das gewünschte Pattern bereitstellt.
Core Pattern
Isolation
Das Isolation Pattern hilft uns dabei, dass entstehende Fehler nicht das gesamte System zum Absturz bringen. Kaskadierende Fehler werden verhindert, es werden Failure Units definiert, in denen diese Fehler auftreten können und behandelt werden. Das Isolation Pattern wird während der Design-Phase angewandt, die Anwendung wird so geschnitten, dass dabei die sogenannten Bulkheads entstehen.
Hystrix hilft uns nicht dabei, ein richtiges Design zu wählen, es erlaubt uns aber kaskadierende Fehler zu verhindern, in dem wir diese in einem Bereich zulassen, der von Hystrix kontrolliert wird.
Spring Cloud Netflix-Umsetzung: Hystrix
Detect Pattern
Circuit Breaker
Der Circuit Breaker kontrolliert und überwacht die Aufrufe eines Remote-Service und kann die Aufrufe im Fehlerfall oder bei einem Timeout in einen bereitgestellten Fallback umleiten. Dabei kontrolliert er über einen bestimmten Zeitraum alle Aufrufe, sollten mehrere Aufrufe scheitern, öffnet sich der Circuit Breaker und lässt für eine definierte Zeit keine weiteren Requests mehr durch. Der Hystrix Circuit Breaker bedient sich dabei des Timeout Patterns und des Fail Fast Pattern, die ebenfalls aus dem Resilience Pattern-Baukasten stammen.
Spring Cloud Netflix-Umsetzung: Hystrix
Monitoring
Betreibe ich eine verteilte Anwendung, so muss ich zu jeder Zeit über den Status meiner einzelnen Komponenten informiert sein und im besten Fall ist das System selbst in der Lage, auf Fehler zu reagieren und stellt entsprechende Fallback-Szenarien bereit. In der Demo habe ich dies durch den Einsatz von Hystrix, Eureka und des Spring Boot Admins realisiert. Der Spring Boot Admin gehört nicht zum Spring Cloud Netflix Stack, gibt mir aber eine sehr gute Übersicht zum Status meiner verteilten Spring Boot-Anwendungen und deren Metriken.
Spring Cloud Netflix-Umsetzung: Hystrix, Eureka (Service Discovery)
Health Check
Beim Healt Check Pattern wird die Verfügbarkeit und Response-Zeit eines Remote Service überwacht. So können im Vorfeld Aufrufe auf eine andere Instanz des Service umgeleitet werden und Fehler verhindert werden. Dies übernimmt im Netflix Stack der Hystrix Circuit Breaker, für den mit ihm abgesicherten Service. Eureka als Service Discovery führt periodische Health Checks durch und nimmt Services aus dem Katalog der verfügbaren heraus, sollten diese nicht mehr erreichbar sein. Diese wichtige Information steht allen Eureka Clients zur Verfügung, die so keine Requests mehr an einen nicht verfügbaren Service routen.
Spring Cloud Netflix-Umsetzung: Hystrix, Eureka (Service Discovery)
Recover Pattern
Retry
Beim Retry Pattern werden im Fehlerfall ein oder mehrere erneute Aufrufe durchgeführt, in Abhängigkeit des Fehlers. Dies lässt sich sehr einfach mit Hystrix umsetzen, da wir uns hier Fallbacks definieren können, die dies inkl. Timeout Pattern durchführen können.
Spring Cloud Netflix-Umsetzung: Hystrix
Failover
Im Failover Pattern wird eine andere Instanz des Service im Fehlerfall aufgerufen, dies setzt natürlich die Redundanz eines Service voraus. Wurden hier Fehler im Design der Anwendung gemacht, werden diese bei der Implementierung eines Failovers sichtbar – wenn auch zu spät.
Spring Cloud Netflix-Umsetzung: Hystrix, Eureka (Service Discovery), Ribbon
Mitigate Pattern
Fallback
Das Fallback Pattern gehört wohl zu den schwierigsten und aufwändigsten Pattern, nicht weil es sehr herausfordernd ist, es lässt sich technisch schnell abhandeln und versetzt das System in einen stabilen Zustand.
Das Ziel dieses Patterns ist es, den Benutzer nicht merken zu lassen, dass unser Backend nur noch aus Trümmern besteht! Daher ist es wichtig, bei der Definition von Fallbacks eng mit dem Fachbereich zusammenzuarbeiten und genau zu definieren, wie der Fallback auszusehen hat.
- Sollen und können wir auch gecachte Ergebnisse zurückgreifen?
- Sollen wir im Fallback von der synchronen auf eine asynchrone Verarbeitung wechseln?
- Sollen wir den Request zwischenspeichern und später verarbeiten?
Diese und andere Fragen gilt es zu beantworten. Die Antwort im Fallback muss fachlich definiert werden. Hier gilt es, die richtigen Entscheidungen zu treffen und sich auch die notwendige Zeit dafür einzuräumen.
Spring Cloud Netflix-Umsetzung: Hystrix
Share Load
Beim Share Load Pattern wird die Last im System verteilt, dies kann durch eine statische, im Code implementierte Logik oder dynamisch erfolgen. Ribbon als Client-Side-Load-Balancer bietet sich hier an und in Verbindung mit einer Service Discovery können wir sein Potential voll ausschöpfen. Die Service Discovery liefert Ribbon die wichtigen Informationen über verfügbare Instanzen eines Service, die dann von Ribbon via Round-Robin-Verfahren mit Requests versorgt werden. So verteilen wir die Last im System und können auf ausgefallene Instanzen reagieren. Ribbon lässt sich auch sehr gut im Fallback von einem Hystrix Command nutzen, indem wir uns einfach die nächste verfügbare Instanz eines Service für unseren Retry geben lassen.
Spring Cloud Netflix-Umsetzung: Ribbon
Complement Pattern
Redundancy
Bei der Redundanz lassen sich die verschiedensten Strategien entwickeln, wie Clustering betreiben, einen Load-Balancer einbauen oder eine Service Discovery in Verbindung mit einem Client-Side-Load-Balancer. Das Ziel bleibt dabei immer gleich, die Applikation durch Redundanz von einzelnen Instanzen abzusichern.
In der Demo geschieht dies durch Eureka und Ribbon, die im Zusammenspiel das Kommen und Gehen von Service-Instanzen abfangen.
Spring Cloud Netflix-Umsetzung: Ribbon, Eureka
Bereich | Pattern | Spring Cloud Netflix |
---|---|---|
Core | Isolation | Hystrix |
Bulkhead | Hystrix | |
Detect | Circuit Breaker | Hystrix |
Timeout | Hystrix | |
Monitoring | Hystrix, Eureka | |
Health Check | Hystrix, Eureka | |
Recover | Retry | Hystrix |
Failover | Hystrix, Eureka, Ribbon | |
Mitigate | Fallback | Hystrix |
Share Load | Ribbon | |
Complement | Redundancy | Ribbon, Eureka |
Einschränkung
Bei allen Pattern gilt: Bedient euch nur derer, die euch einen Mehrwert bieten und versucht nicht, auf Biegen und Brechen jedes Pattern anzuwenden. Es muss immer abgewogen werden, ob das Pattern die gewünschte Stabilität und Verbesserung liefert oder ob man sich unnötige Komplexität ins Haus holt.
Demo – Transport my Package
Nachdem wir uns nun oberflächlich mit den verwendeten Resilience Pattern beschäftigt haben, wird es Zeit, diese im Zusammenspiel zu erleben. Die schon mehrfach erwähnte Demo hat die Aufgabe, Transport-Aufträge zu erfassen und der Sendung dabei eine eindeutige Sendungsnummer (Connote) zu verpassen. Eine Buchung kann dabei nur erfolgen, wenn die Abhol- und Zustelladresse valide sind und wir zur Kundennummer auch einen passenden Kunden im System haben.
Gehen wir zunächst die notwendigen Schritte eines Booking Requests durch:
- Step 1: Validierung der Abhol- und Zustelladresse, überprüft, ob es sich um eine Adresse, die unser junges Unternehmen auch anfahren kann, handelt.
- Step 2: Simple Überprüfung, ob die im Request vorhandene Kundennummer im CustomerService existiert.
Die Steps 1 und 2 werden dabei parallel ausgeführt und liefern uns die benötigten Daten um mit Step 3 fortzufahren. Nur bei einem Erfolg beider Steps kann die eigentliche Buchung durchgeführt werden.
- Step 3: Der Booking Service übernimmt die im Step 1 & 2 validierten und angereicherten Daten und bereitet eine Buchung vor.
- Step 4: Erzeugen einer eindeutigen Frachtbriefnummer, unter der die Sendung geführt wird.
- Step 5: Fortschreiben der Buchung, wenn eine Frachtbriefnummer erzeugt werden konnte. Im Fehlerfall wird eine entsprechende Response zurückgegeben.
Architektur
In Abb.3 ist der Aufbau unserer Transport-Service dargestellt, ich habe bewusst nicht jede Beziehung visualisiert und stelle zunächst nur die wichtigsten Akteure vor.
Chaos Monkey
Die Demo ist in angemessener Zeit gebaut und funktioniert, aber irgendwie fehlt da ein wenig Leben in der Bude. Es ist doch sehr ernüchternd und langweilig für einen Entwickler, eine neue Anwendung zu gestalten und nie zu erleben, wie die Anwendung mit zufälligem Chaos umgeht. Ein ungutes Bauchgefühl bleibt, sollte man die entwickelte Anwendung nie unter Stress und im Zusammenspiel gesehen haben.
Viel besser wäre es doch, die Anwendung durch einen kleinen Helfer und ein paar seiner bösen Freunde in einen instabilen Zustand zu versetzen, um zu sehen, ob die unabhängigen Module und die daraus entstehenden Bulkheads, der Einsatz von Hystrix, Eureka und Ribbon auch wirklich funktioniert.
Bei Netflix hat dies zur Simian Army [3] geführt, mit der sich jeder Entwickler messen lassen und deren Angriffen jedes entwickelte System standhalten muss. Einer der bekanntesten Protagonisten ist ein Kollege mit dem passenden Namen Chaos Monkey. Bei Netflix hat er die Aufgabe, AWS-Instanzen im laufenden Betrieb abzuschießen und so sicherzustellen, dass die Entwickler den aufrufenden Service entsprechend robust – sprich resilient – gebaut haben. Diesen Ansatz finde ich persönlich sehr interessant und ich habe mir daher einen eigenen Chaos Monkey für Spring geschrieben. Dieser entspricht jetzt nicht dem Netflix Chaos Monkey, da mein Monkey innerhalb meiner Spring Boot-Anwendung läuft und diese auch nicht komplett zerstören wird. Er sorgt aber für ein ungewolltes Verhalten, auf das ich reagieren muss.
Mit Hilfe einer einfachen Annotation @EnableChaosMonkey [4] kann ich in meiner Spring Boot-Applikation einen Chaos Monkey bereitstellen, den ich mit Hilfe von Archaius dynamisch zur Laufzeit aktivieren kann. Sollte der Chaos Monkey aktiviert sein, wird er per Zufall darüber entscheiden, wie er uns das Leben zur Hölle machen möchte.
Den Chaos Monkey habe ich in den folgenden Services aktiviert und hoffe auf ordentlich Chaos.
Folgendes Verhalten ist per Zufall vom Chaos Monkey zu erwarten:
- RuntimeException
- Timeout
- nichts
Im besten Fall haben wir also nichts zu befürchten und können auf eine funktionierende Anwendung "hoffen". Dies wird mit den aktuellen Einstellungen, mit denen der Chaos Monkey läuft, aber nicht allzu oft passieren. Er wird sich an jede Komponente der Beispielanwendung hängen, die einen Service bereitstellt und dort jede public-Methode beeinflussen.
Nur so ist es uns möglich, Hystrix im tatsächlichen Einsatz zu sehen und zu der Erkenntnis zu gelangen, ob sich der Einsatz von Hystrix und allen anderen Komponenten aus dem Spring Cloud Netflix Stack wirklich lohnt.
Service Discovery – Eureka
Wie man am Aufbau der einzelnen Servicekomponenten erkennen kann, habe ich mich für einen synchronen Request/Response-Aufbau entschieden. Die Komponenten kommunizieren via REST miteinander und nutzen zur Absicherung der Remote-Calls Hystrix. Alle Services registrieren sich bei der zentralen Service Discovery (Eureka), diese führt einen permanenten Health-Check aller Services durch. Sollte ein Service ausfallen oder der periodische Health-Check erkennen, dass der Service nicht mehr innerhalb der erlaubten Zeit antworten, wird dieser aus dem Katalog der verfügbaren Services entfernt. Alle Eureka Clients bekommen diese Information zugespielt und werden den Service vorerst nicht mehr aufrufen. Sollte dieser sich wieder normalisieren, bietet Eureka den Service wieder an. Ebenfalls möglich ist das gezielte Entfernen von Services, um hier zum Beispiel ein Update durchzuführen.
Client-Side-Load-Balancer – Ribbon
Ribbon als Client-Side-Load-Balancer ist in der Lage, eine verfügbare Instanz des benötigten Remote-Service zu ermitteln, dies geschieht in Verbindung mit der Service Discovery Eureka. Eureka ist in der Lage, zu erkennen, ob ein Service Requests verarbeiten kann. Diese Information wird den Eureka Clients, welche sich Ribbon zu nutze macht, bereitgestellt. Eureka kann und sollte in einem Cluster betrieben werden, auch wenn die Clients ohne eine laufende Eureka Service-Instanz in der Lage sind, die Requests auszuführen. Dies wird durch einen lokalen Cache in den Eureka Clients ermöglicht, den der Client beim Hochfahren des Service aufbaut und permanent refreshed.
Ribbon setzt per Default auf das Round-Robin-Verfahren und routet die Requests so auf alle Instanzen eines Service, es verteilt so gezielt die Last, unter Berücksichtigung der Verfügbarkeiten.
Ein weiterer Vorteil der Service Discovery ist die lose Kopplung der Systeme, der Entwickler muss nicht mehr wissen unter welcher IP und welchem Port er den benötigten Service oder noch viel schlimmer, wie er die verschiedenen Instanzen eines Service erreicht. Er benötigt nur noch den Namen des Service, mit dem er kommunizieren will, unabhängig von der Umgebung, in der er unterwegs ist.
In modernen Microservice-Landschaften kommen und gehen Instanzen eines Service, wie es die aktuelle Last auf dem System erfordert. Die Architektur wird so gewählt, dass das System selbst entscheiden kann, ob es von einem aktuellen Service mehr Instanzen zum Zweck des Share-Loads benötigt.
Hystrix
Hystrix stammt ebenfalls aus dem Hause Netflix und ist als eines von ca. 30 anderen Open Source-Projekten veröffentlicht worden. In meiner Beispielanwendung habe ich Hystrix bei allen Remote-Calls verwendet, um mich vor deren Ausfällen und Fehlern zu schützen. Ich habe mit dem Default Timeout von 1 Sekunde gearbeitet, wie in der Abbildung zu erkennen ist.
Wann und wofür sollte ich Hystrix einsetzen, sollte eine der ersten Fragen sein, die es zu beantworten gilt: Hystrix kann mit Java, Java EE und Spring eingesetzt werden und wird als Dependency im Projekt eingebunden. Egal in welcher Galaxie des Java-Universums ich mich nun aufhalte, habe ich immer die drei Möglichkeiten, Hystrix in meinem Code zu verankern.
Als erstes stößt man auf das Command-Pattern [5], wobei für jeden externen Service-Aufruf ein eigenes Command erstellt wird und dort der Service-Aufruf mit passendem Fallback bereitgestellt wird.
Fallbacks
Die Fallbacks sind die hohe Kunst des Ganzen und müssen mit der Fachlichkeit zusammen entwickelt werden. Hier trifft die Fachlichkeit auf technische Fehler, die vom Entwickler erklärt werden und zu einem definierten Fallback führen müssen. Als Entwickler habe ich ebenso die Möglichkeit, Hystrix-Javanica [6] zu nutzen und anstatt einer eigenen Klasse mit einer einfachen @HystrixCommand-Annotation zu arbeiten. Bewege ich mich in der Spring-Galaxie, habe ich hier, wie für Spring typisch, auch die Möglichkeit, mit einer einfachen @HystrixCommand Annotation zu arbeiten. Spring baut dabei auf der Hystrix-Javanica-Implementierung auf. Zu erwähnen sei dabei, dass ich meine Fallback-Methode als einfachen String in der Annotation angebe und somit erst zur Compile-Zeit einen möglichen Tippfehler im Methodennamen erkenne.
Das schöne an Hystrix ist, dass Netflix eine Unmenge an Konfigurationsmöglichkeiten vorgesehen hat und für jeden Parameter einen passenden Default bereitstellt. Somit ist es möglich, Hystrix aus dem Stand heraus zu nutzen und mit den Defaults erstmal loszulegen.
Hystrix-Metriken
Ein weiteres sehr nützliches Feature von Hystrix sind die Metriken, die wir frei Haus geliefert bekommen. Hystrix muss für die korrekte Zustandsberechnung des Circuit Breakers die Aufrufe monitoren und für einen definierten Zeitraum vorhalten. Diesen Zeitraum können wir, wie von Hystrix gewohnt, an unsere Bedürfnisse anpassen.
Dank unseres Chaos Monkeys haben wir richtig Leben in der Bude und sehen Hystrix in Aktion. Dies wird sich auch in den Metriken von Hystrix widerspiegeln. Wie können wir uns diese Metriken nun zunutze machen und vor allem: wie kommen wir an diese sehr wichtigen Daten?
Jede Komponente, in der wir Hystrix einsetzen, kann uns einen HTTP Stream mit allen Metriken liefern. Dies wäre einer der Wege, die wir gehen können. Da wir aber eine verteilte Anwendung haben und nicht immer wissen können, unter welcher IP und mit welchem Port die Anwendung läuft, wäre es doch viel schöner, wenn die Anwendung uns die Daten über einen definierten Kanal liefert.
In der Beispielanwendung habe ich mich für einen Messaging-Weg entschieden und RabbitMQ eingesetzt. Dies hat den Vorteil, dass alle Komponenten die Daten in einer Queue ablegen, an der sich die Hystrix-Turbine [7] als Consumer registriert und die Daten aggregiert zurückliefert.
Die Turbine hat dabei die Aufgabe, alle eintreffenden Metriken zu einem zentralen Stream zu aggregieren, der dann vom Hystrix Dashboard visualisiert oder anderen Consumern gespeichert, aufbereitet und ausgewertet werden kann. Somit können wir in den Metriken auch erkennen, wie viele Instanzen unseres Service laufen und wie viele Requests wir über alle Instanzen eines Service hinweg haben.
Hystrix Dashboard
Als eines der ersten und vor allem schnell in Betrieb zu nehmenden Monitoring-Tools ist das Hystrix Dashboard zu nennen. Hiermit ist es uns sehr einfach und schnell möglich, einen umfangreichen Überblick über den Zustand unserer Anwendung zu erhalten. Ein Gesamtbild der verteilten Anwendung erhalten wir aber nur, wenn wir uns als Datenquelle den Turbine Stream mit allen aggregierten Streams unserer verteilten Anwendung nehmen.
Fazit
Ich hoffe, ich konnte einen guten und vor allem praxisnahen Überblick zum Thema Spring Cloud Netflix geben. Zu Hystrix möchte ich noch sagen, dass Hystrix kein Allheilmittel ist und auch zu einem unerwünschten Verhalten führen kann. So ist es zum Beispiel möglich, dass sehr viele Requests auf Grund einer falschen Hystrix-Konfiguration in einem Fallback landen. Um dies zu verhindern, müssen die Metriken herangezogen werden und es erfordert auch viel Zeit und Vorbereitung, die richtigen Werte der Konfiguration zu ermitteln.
Viel Spaß mit der Demo!
- Demo auf GitHub
- Informatik Aktuell – Uwe Friedrichsen: Resilient Software Design – Robuste Software entwickeln
- Netflix: Simian Army
- Annotation @EnableChaosMonkey
- Netflix: Command-Pattern
- Netflix: Hystrix-Javanica
- Netflix: Hystrix-Turbine