1. Überblick
In diesem Tutorial sprechen wir über die Bibliothek Resilience4j .
-
Die Bibliothek unterstützt Sie bei der Implementierung widerstandsfähiger Systeme, indem Sie die Fehlertoleranz für die Fernkommunikation verwalten. **
Die Bibliothek ist inspiriert von Hystrix , bietet jedoch eine viel bequemere API und eine Reihe weiterer Funktionen wie Rate Limiter (zu häufige Anfragen blockieren), Bulkhead (ebenfalls vermeiden) viele gleichzeitige Anfragen) usw.
2. Maven-Setup
Zu Beginn müssen wir die Zielmodule zu unserer pom.xml hinzufügen (z. B. fügen wir den Circuit Breaker hinzu) :
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
<version>0.12.1</version>
</dependency>
Hier verwenden wir das __circuitbreaker __Modul. Alle Module und ihre neuesten Versionen finden Sie unter Maven Central .
In den nächsten Abschnitten werden die am häufigsten verwendeten Module der Bibliothek durchlaufen.
3. Leistungsschalter
Beachten Sie, dass wir für dieses Modul die oben gezeigte Abhängigkeit von resilience4j-circuitbreaker benötigen.
Das Circuit Breaker-Muster hilft uns, eine Kaskade von Fehlern zu verhindern, wenn ein Remote-Dienst ausfällt.
-
Nach einer Reihe von fehlgeschlagenen Versuchen können wir davon ausgehen, dass der Dienst nicht verfügbar/überlastet ist, und alle weiteren Anforderungen ** daran ablehnen. Auf diese Weise können wir Systemressourcen für Anrufe einsparen, die wahrscheinlich fehlschlagen.
Mal sehen, wie wir das mit Resilience4j erreichen können.
Zuerst müssen wir die Einstellungen definieren, die verwendet werden sollen. Der einfachste Weg ist die Verwendung von Standardeinstellungen:
CircuitBreakerRegistry circuitBreakerRegistry
= CircuitBreakerRegistry.ofDefaults();
Es ist auch möglich, benutzerdefinierte Parameter zu verwenden:
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(20)
.ringBufferSizeInClosedState(5)
.build();
Hier haben wir den Tarifschwellenwert auf 20% und eine Mindestanzahl von 5 Anrufversuchen eingestellt.
Dann erstellen wir ein CircuitBreaker -Objekt und rufen den Remote-Service über dieses auf:
interface RemoteService {
int process(int i);
}
CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config);
CircuitBreaker circuitBreaker = registry.circuitBreaker("my");
Function<Integer, Integer> decorated = CircuitBreaker
.decorateFunction(circuitBreaker, service::process);
Lassen Sie uns abschließend sehen, wie dies durch einen JUnit-Test funktioniert.
Wir werden versuchen, den Dienst zehnmal anzurufen. Wir sollten in der Lage sein zu überprüfen, dass der Anruf mindestens fünfmal versucht wurde, und dann angehalten, sobald 20% der Anrufe fehlgeschlagen sind:
when(service.process(any(Integer.class))).thenThrow(new RuntimeException());
for (int i = 0; i < 10; i++) {
try {
decorated.apply(i);
} catch (Exception ignore) {}
}
verify(service, times(5)).process(any(Integer.class));
3.1. Zustände und Einstellungen des Leistungsschalters **
Ein CircuitBreaker kann sich in einem der drei Zustände befinden:
-
CLOSED - alles ist gut, kein Kurzschluss
-
OPEN - Remote-Server ist inaktiv, alle Anfragen an ihn sind kurzgeschlossen
-
HALF OPEN__ - eine konfigurierte Zeit seit dem Eintritt in den Zustand OPEN
ist abgelaufen, und CircuitBreaker ermöglicht Anfragen, zu überprüfen, ob der Remote-Dienst wieder online ist
Wir können die folgenden Einstellungen konfigurieren:
-
die Ausfallratenschwelle, oberhalb derer der CircuitBreaker geöffnet wird
beginnt mit dem Kurzschließen von Anrufen ** Die Wartezeit, die definiert, wie lange der CircuitBreaker dauern soll
offen bleiben, bevor es halb geöffnet wird ** die Größe des Ringspeichers, wenn der CircuitBreaker halb geöffnet ist oder
geschlossen ** ein benutzerdefinierter CircuitBreakerEventListener , der CircuitBreaker handhabt
Veranstaltungen ** ein benutzerdefiniertes Predicate , das auswertet, ob eine Ausnahme als
Ausfall und damit die Ausfallrate erhöhen
4. Ratenbegrenzer
Ähnlich wie im vorherigen Abschnitt erfordert diese Funktion die Abhängigkeit von resilience4j-ratelimiter .
Wie der Name schon sagt, erlaubt diese Funktion den Zugriff auf einen Dienst einzuschränken . Seine API ist CircuitBreaker’s sehr ähnlich - es gibt die Klassen Registry , Config und Limiter .
Hier ist ein Beispiel, wie es aussieht:
RateLimiterConfig config = RateLimiterConfig.custom().limitForPeriod(2).build();
RateLimiterRegistry registry = RateLimiterRegistry.of(config);
RateLimiter rateLimiter = registry.rateLimiter("my");
Function<Integer, Integer> decorated
= RateLimiter.decorateFunction(rateLimiter, service::process);
Nun müssen alle Anrufe auf dem dekorierten Dienstblock ggf. an die Konfiguration des Geschwindigkeitsbegrenzers angepasst werden.
Wir können Parameter konfigurieren wie:
-
der Zeitraum der Limitaktualisierung
-
Die Berechtigungsgrenze für den Aktualisierungszeitraum
-
die Standardwartezeit für die Genehmigung
5. Schott
Hier benötigen wir zunächst die resilience4j-bulkhead Abhängigkeit.
Sie können die Anzahl der gleichzeitigen Anrufe zu einem bestimmten Dienst begrenzen.
Sehen Sie sich ein Beispiel für die Verwendung der Bulkhead-API zum Konfigurieren einer maximalen Anzahl von gleichzeitigen Aufrufen an:
BulkheadConfig config = BulkheadConfig.custom().maxConcurrentCalls(1).build();
BulkheadRegistry registry = BulkheadRegistry.of(config);
Bulkhead bulkhead = registry.bulkhead("my");
Function<Integer, Integer> decorated
= Bulkhead.decorateFunction(bulkhead, service::process);
Um diese Konfiguration zu testen, rufen wir die Methode eines Mock-Services auf.
Dann stellen wir sicher, dass Bulkhead keine weiteren Anrufe zulässt:
CountDownLatch latch = new CountDownLatch(1);
when(service.process(anyInt())).thenAnswer(invocation -> {
latch.countDown();
Thread.currentThread().join();
return null;
});
ForkJoinTask<?> task = ForkJoinPool.commonPool().submit(() -> {
try {
decorated.apply(1);
} finally {
bulkhead.onComplete();
}
});
latch.await();
assertThat(bulkhead.isCallPermitted()).isFalse();
Wir können die folgenden Einstellungen konfigurieren:
-
die maximale Anzahl paralleler Ausführungen, die das Schott zulässt
-
die maximale Zeitdauer, auf die ein Thread wartet, wenn er versucht, einzutreten
ein gesättigtes Schott
6. Wiederholen
Für diese Funktion müssen Sie die Bibliothek resilience4j-retry dem Projekt hinzufügen.
Wir können einen fehlgeschlagenen Aufruf automatisch mit der Retry-API wiederholen:
RetryConfig config = RetryConfig.custom().maxAttempts(2).build();
RetryRegistry registry = RetryRegistry.of(config);
Retry retry = registry.retry("my");
Function<Integer, Void> decorated
= Retry.decorateFunction(retry, (Integer s) -> {
service.process(s);
return null;
});
Lassen Sie uns nun eine Situation emulieren, in der während eines Remote-Service-Aufrufs eine Ausnahme ausgelöst wird, und sicherstellen, dass die Bibliothek den fehlgeschlagenen Aufruf automatisch erneut versucht:
when(service.process(anyInt())).thenThrow(new RuntimeException());
try {
decorated.apply(1);
fail("Expected an exception to be thrown if all retries failed");
} catch (Exception e) {
verify(service, times(2)).process(any(Integer.class));
}
Wir können auch Folgendes konfigurieren:
-
die Anzahl der Versuche
-
die Wartezeit vor erneuten Versuchen
-
eine benutzerdefinierte Funktion zum Ändern des Warteintervalls nach einem Fehler
-
ein benutzerdefiniertes Predicate , das auswertet, ob eine Ausnahme resultieren soll
Wiederholen Sie den Anruf
7. Zwischenspeicher
Das Cache-Modul erfordert die Abhängigkeit von resilience4j-cache .
Die Initialisierung sieht etwas anders aus als die anderen Module:
javax.cache.Cache cache = ...;//Use appropriate cache here
Cache<Integer, Integer> cacheContext = Cache.of(cache);
Function<Integer, Integer> decorated
= Cache.decorateSupplier(cacheContext, () -> service.process(1));
Hier erfolgt die Zwischenspeicherung durch die Implementierung von JSR-107 Cache , und Resilience4j bietet eine Möglichkeit, es anzuwenden.
Beachten Sie, dass es keine API zum Dekorieren von Funktionen gibt (wie Cache.decorateFunction (Function) ). Die API unterstützt nur die Typen Supplier und Callable .
8. Zeitlimit
Für dieses Modul müssen wir die Abhängigkeit von resilience4j-timelimiter hinzufügen.
Es ist möglich, die Zeit einzuschränken, die ein Remote-Service mit dem TimeLimiter angerufen hat.
Um dies zu demonstrieren, lassen Sie uns einen TimeLimiter mit einem konfigurierten Timeout von 1 Millisekunde einrichten:
long ttl = 1;
TimeLimiterConfig config
= TimeLimiterConfig.custom().timeoutDuration(Duration.ofMillis(ttl)).build();
TimeLimiter timeLimiter = TimeLimiter.of(config);
Als Nächstes überprüfen wir, ob Resilience4j Future.get () mit dem erwarteten Timeout aufruft:
Future futureMock = mock(Future.class);
Callable restrictedCall
= TimeLimiter.decorateFutureSupplier(timeLimiter, () -> futureMock);
restrictedCall.call();
verify(futureMock).get(ttl, TimeUnit.MILLISECONDS);
Wir können es auch mit CircuitBreaker kombinieren:
Callable chainedCallable
= CircuitBreaker.decorateCallable(circuitBreaker, restrictedCall);
9. Zusatzmodule
Resilience4j bietet auch eine Reihe von Zusatzmodulen an, die die Integration mit gängigen Frameworks und Bibliotheken erleichtern.
Einige der bekannteren Integrationen sind:
-
Spring Boot - resilience4j-Spring-Boot -Modul
-
Ratpack - resilience4j-ratpack -Modul
-
Retrofit - Modul resilience4j-retrofit
-
Vertx - resilience4j-vertx -Modul
-
Dropwizard - Modul resilience4j-metrics
-
Prometheus - resilience4j-prometheus -Modul
10. Fazit
In diesem Artikel haben wir uns mit verschiedenen Aspekten der Resilience4j-Bibliothek befasst und gelernt, wie diese zur Behebung verschiedener Fehlertoleranzprobleme bei der Kommunikation zwischen Servern verwendet werden kann.
Den Quellcode für die obigen Beispiele finden Sie wie immer unter https://github.com/eugenp/tutorials/tree/master/libraries.html auf GitHub].