Die Anforderungen an Stabilität von Software sind in den letzten Jahren stetig gestiegen. Globalisierung, Dynamisierung von Arbeitszeit und auch die Verbreitung von Mobilgeräten haben dafür gesorgt, dass von vielen Systemen erwartet wird, dass diese dauerhaft verfüg- und nutzbar sind.
Gleichzeitig haben wir es mit immer komplexeren Systemen zu tun. Neben unserem eigenen Code setzen wir Fremdbibliotheken und Frameworks ein. Zudem ist die Anbindung von anderen Systemen über das Netzwerk fast immer gegeben. Wir haben es also mit einem verteilten System zu tun.
Wo wir vor einem Jahrzehnt das Netzwerk noch häufig als stabil und immer vorhanden betrachtet haben, erkennen wir nun, auch durch Cloudumgebungen, an, dass wir uns darauf nicht verlassen können. Diese Kombination ermöglicht eine Menge an Fehlern, auf die unsere Anwendung vorbereitet sein sollte, um die im ersten Absatz geforderten Qualitäten zu erfüllen.
Der Fachbegriff lautet hier Resilience. Mir ist der Begriff das erste Mal im Buch „Release It” von Michael T. Nygard begegnet. Obwohl das Buch in seiner ersten Edition bereits 2007 erschienen ist, ist es heute aktueller denn je. Richtig eingesetzt, erhöhen die dort bereits beschriebenen Muster die Stabilität unseres Systems deutlich.
Auf die JVM haben es die Muster vor allem durch die bekannte Bibliothek Hystrix geschafft. Da diese sich jedoch bereits seit zwei Jahren im Maintenance-Modus befindet, wollen wir uns in diesem Artikel mit Resilience4j eine aktuellere Alternative anschauen und wie die aus Hystrix bekannten Muster dort umgesetzt werden.
Danke
Nach mittlerweile fünf Jahren Kolumne möchte ich hier einmal allen danken, die mich unterstützen und dies möglich machen.
Zuerst möchte ich mich bei meiner Frau Nadine bedanken, die mich auch dann unterstützt, wenn ich rund um die Deadline alle zwei Monate schlechte Laune bekomme und mich quäle, um die Kolumne fertig zu bekommen.
Auch Frau Weinert, meine Lektorin, muss darunter leiden, wenn ich dann verspätet meinen Text einreiche und sie anschließend sämtliche Kommas noch einmal verschieben muss. Danke dafür.
Weiterhin möchte ich den vielen Kolleginnen und Kollegen von INNOQ danken, die immer ein offenes Ohr für mich haben, mich motivieren oder mit mir über Themen diskutieren. Ohne euch wüsste ich nicht, wie ich bereits 30 Themen zum Schreiben gefunden hätte. Danke Phillip, Stefan, Lisa, Torsten, Martin, Joy, Thomas, Till und allen anderen.
Danke auch an Emanuel und Michael, die mir die Kolumne anvertraut haben und seitdem darauf vertrauen, dass ich es alle zwei Monate schaffe, einen sinnvollen Text einzureichen.
Und zuletzt möchte ich mich noch bei Ihnen bedanken. Danke, dass Sie meine Texte lesen und diese zumindest nicht offensichtlich verreißen. Sollten Sie doch einmal einen Fehler finden oder nicht einverstanden sein, freue ich mich, wie gehabt, über Ihr Feedback in jeglicher Form.
Danke!
Hystrix
Mit Hystrix hat Netflix bereits Ende 2012 die vermutlich erste und wohl
bekannteste Bibliothek auf der JVM für Widerstandsfähigkeit zur Verfügung
gestellt. Im Kern stellt Hystrix uns dabei die Klasse HystrixCommand
zur
Verfügung, welche von uns implementiert wird und anschließend unseren Code durch
eine Kombination der Muster Fallback, Timeout, Circuit Breaker und
Bulkhead absichert (s. Listing 1).
Für eine detailliertere Ausführung zu Hystrix empfehle ich den Artikel „Hystrix – damit Ihnen rechtzeitig die Sicherung durchbrennt” von zwei meiner Kollegen.
Seit Ende 2018 befindet sich Hystrix jedoch im Maintenance-Modus, wird also nicht mehr weiterentwickelt. Für neue Projekte wird der Einsatz von Resilience4j empfohlen.
Im Gegensatz zu Hystrix, das mit Archaius immer die Netflix eigene Konfigurationsbibliothek mitbrachte, kommt Resilience4j bis auf VAVR ohne externe Abhängigkeiten aus und hat somit einen deutlich kleineren Fußabdruck. Zudem modelliert Resilience4j die Stabilitätsmuster einzeln und erlaubt es uns somit, eine beliebige Kombination zusammenzustellen, die genau zum Anwendungsfall passt.
Im Folgenden wollen wir uns anschauen, wie wir mit Resilience4j die vier von Hystrix kombinierten Muster implementieren können.
Fallback
Immer wenn ein Fehler passiert, müssen wir uns die Frage stellen, wie unsere Anwendung darauf reagieren soll.
Eine Antwort, die vermutlich einfachste, hierauf ist es, den Fehler den Anwendern anzuzeigen und diese entscheiden zu lassen, was sie tun wollen. Vielfach ist es jedoch besser, sich eine Alternative, einen Fallback, zu überlegen. Können wir beispielsweise zur Bonitätsprüfung das externe System von Anbieter A nicht erreichen, kann es sinnvoll sein, das teurere System von Anbieterin B anzufragen. Auch wäre es hier möglich, anhand der uns zur Verfügung stehenden Daten eine eigene, dafür jedoch simplere und vermutlich schlechtere Entscheidung zu treffen.
Wichtig ist dabei, dass wir bei der Betrachtung stets die Fachlichkeit und den Fachbereich einbeziehen. Nur dieser kann in unserem Beispiel oben entscheiden, ob es sich lohnt, eine teurere oder schlechtere Überprüfung durchzuführen oder die Bestellung abzulehnen, da gerade keine Überprüfung der Bonität durchführbar ist.
Fallbacks werden von Resilience4j nicht durch ein eigenes Konzept abgebildet,
sondern durch das Fangen von Exceptions oder die Nutzung von VAVRs Try
in
unserem Code umgesetzt.
Timeout
Jeder Aufruf, der länger dauert, bindet innerhalb unserer Anwendung Ressourcen in Form von Prozessen oder Threads. Da diese, schon hardwarebedingt, nur endlich verfügbar sind, kann eine Anwendung, bei der alle Ressourcen ausgelastet sind, nicht mehr reagieren oder neue Anfragen entgegennehmen.
Deshalb ist es sinnvoll, für Aufrufe, die lange dauern können, Timeouts zu definieren. Der Aufruf wird somit spätestens nach einer definierten Zeitspanne mit einem Fehler abgebrochen. Damit verhindern wir, dass unsere Anwendung nicht mehr reagieren kann, müssen uns jedoch anschließend Gedanken machen, wie wir fachlich auf diese Fälle reagieren, also einen Fallback einbauen.
Im Idealfall sollten wir mindestens unsere Netzwerkaufrufe mit einem Timeout
versehen und das idealerweise innerhalb der von uns verwendeten Bibliothek.
Sollte das nicht möglich sein, können wir auf den Support von Resilience4j und
der dort vorhandenen TimeLimiter
-Komponente (s. Listing 2) zurückgreifen.
Um eine Instanz von TimeLimiter
zu erzeugen, müssen wir eine
TimeLimiterRegistry
verwenden. Diese wiederum erhält eine vorher erzeugte
Konfiguration, in diesem Falle mit einem Timeout von drei Sekunden. Auf dem so
erzeugten TimeLimiter
haben wir nun Methoden zur Verfügung, um eine übergebene
Methode mit dem Timeout abgesichert auszuführen. Häufig ist es jedoch
sinnvoller, wie in Listing 2 zu sehen, unseren Code über eine statische Methode
zu dekorieren und eine Methodenreferenz zu erhalten, die wir dann später
aufrufen können. In diesem Fall wird dabei eine TimeoutException
geworfen, da
unser Code länger als der definierte Timeout braucht.
Diese Art der Programmierschnittstelle verwendet Resilience4j auch bei allen
weiteren Mustern. Erst erzeugen wir eine Registry mit einer Konfiguration. Mit
dieser können wir anschließend eine benannte Musterinstanz erzeugen. Diese
enthält Methoden, die mit dem Wort execute
anfangen und unseren Code direkt,
durch das Muster abgesichert, ausführen. Alternativ gibt es auf dem Interface
des Musters noch statische Methoden, die mit dem Wort decorate
beginnen und
uns eine abgesicherte Methodenreferenz zurückgeben.
Circuit Breaker
Die beiden vorherigen Muster helfen uns zwar dabei, nicht selber lange zu warten und auch ein kurzzeitig nicht erreichbares anderes System auszuhalten, das Gesamtsystem stabilisieren sie jedoch nicht.
Nehmen wir an, das andere System hat gerade Probleme, weil es überlastet ist, und kann deswegen nicht mehr schnell genug antworten. Gerade in solchen Fällen führt unsere Strategie dazu, dieses System noch mehr zu befeuern und damit das Gesamtproblem noch zu verschärfen.
Um dies zu verhindern, können wir einen Circuit Breaker einsetzen. Dieser funktioniert dabei wie eine Sicherung im Sicherungskasten. Solange die Aufrufe fehlerfrei funktionieren, bleibt diese im geschlossenen Zustand. Sobald jedoch ein vorher definierter Schwellwert überschritten wird, wechselt die Sicherung in den offenen Zustand. In diesem wird jeder neue Aufruf direkt abgewiesen und gar nicht erst versucht. Somit entlasten wir das Ziel des Aufrufs und geben ihm Zeit, sich zu erholen.
Im Gegensatz zu einer physischen besitzt eine solche digitale Sicherung jedoch noch einen dritten, den halb-offenen Zustand. In diesen wechselt eine vorher geöffnete Sicherung nach einer definierten Zeitdauer oder Anzahl von Aufrufen und lässt anschließend eine kleine Menge von Aufrufen durch, lehnt den Großteil jedoch weiterhin ab. Abhängig vom Ergebnis dieser durchgelassenen Aufrufe entscheidet die Sicherung anschließend, ob sie erneut in den offenen oder wieder in den geschlossenen Zustand wechselt.
Listing 3 zeigt uns die Verwendung dieses Musters in Resilience4j. Hierbei betrachtet die Sicherung immer das Fenster der letzten vier Aufrufe, um zu entscheiden, wie ihr Zustand ist. Sind dabei mindestens zwei Aufrufe vorhanden und mehr als 75 Prozent dieser Aufrufe sind fehlerhaft, wird in den offenen Zustand gewechselt. Von diesem wechselt sie nach einer Sekunde in den halb-offenen Zustand und lässt hier zwei Aufrufe durch.
Bulkhead
Wie bereits bei den Timeouts beschrieben, ist eines der häufigsten Probleme, wieso ein System nicht reagiert, darin begründet, dass alle Prozesse/Threads ausgelastet sind und wir keine weitere Abarbeitung durchführen können.
In vielen Anwendung gibt es jedoch kritischere und weniger kritischere Anwendungsfälle. Mit Bulkheads können wir diese nun voneinander isolieren und jedem eine gewisse Menge an Ressourcen zuweisen. Somit sollte eine Vollauslastung von Anwendungsfall A nicht mehr dazu führen, dass auch Anwendungsfall B nicht mehr aufgerufen werden kann. Das System funktioniert somit nur noch in Teilen, ist aber in seiner Summe widerstandsfähiger, da es nicht komplett ausgefallen ist.
Das Pendant in der realen Welt sind die Schotts auf Schiffen. Diese befinden sich im Rumpf und sind durch Wände voneinander getrennt. Sollte das Schiff ein Leck haben, füllen sich nur Teile und nicht direkt der gesamte Rumpf mit Wasser und das Schiff sinkt nicht sofort.
Wie in Listing 4 zu sehen, greift Resilience4j auch hier wieder das bekannte Muster auf. Hier erlauben wir in der Konfiguration lediglich zwei gleichzeitige Aufrufe und sorgen dafür, dass im Falle von Vollauslastung neue Aufrufe maximal vier Sekunden lang warten, bis diese abgewiesen werden.
Mehr Resilience4j
Neben den vier von Hystrix unterstützten Mustern bietet uns Resilience4j darüber hinausgehend noch Unterstützung für Retries, Rate Limits und Caching. Aus Platzgründen werden diese hier nicht behandelt, befinden sich aber als Beispiele, wie auch der hier gezeigte Code, unter https://github.com/mvitz/javaspektrum-resilience.
Zudem bietet uns Resilience4j für gängige Frameworks wie Spring Boot oder Micronaut auch tiefere Integrationsmöglichkeiten als den hier gezeigten programmatischen Zugriff an. Hier kommen dann häufig Annotationen zum Einsatz, mit denen die Methoden unseres Codes annotiert und damit um die Stabilitätsaspekte ergänzt werden. Außerdem können wir die von Resilience4j während der Laufzeit zur Verfügung gestellten Metriken mit einem zusätzlichen Modul in Micrometer und damit in nahezu alle gängigen Monitoring-Systeme integrieren.