Inhalt
Moderne Anwendungen werden immer seltener als monolithische Blöcke entwickelt. Stattdessen setzen wir zunehmend auf verteilte Systeme und Microservices. Das bietet ein hohes Maß an Skalierbarkeit und Flexibilität, bringt jedoch auch neue Herausforderungen mit sich.
DNS-Probleme, abgelaufene Zertifikate, Dienstausfälle, überlastete Datenbanken, hohe Latenzen externer Systeme oder Netzwerkprobleme gehören in verteilten Systemen zum Alltag. Eine der größten Gefahren dabei sind Kaskadenausfälle: Ein einzelner langsamer oder nicht erreichbarer Service kann wie ein Domino-Stein weitere Teile des Systems beeinträchtigen und im schlimmsten Fall das gesamte System lahmlegen.
Antwortet beispielsweise ein externer Buchungsdienst immer langsamer, warten im aufrufenden Service zunehmend mehr Threads auf eine Antwort. Dadurch können sich der Thread-Pool und der HTTP-Connection-Pool nach und nach füllen. Neue Anfragen können schließlich nicht mehr oder nur noch sehr langsam verarbeitet werden. Aus einem lokalen Problem des Buchungsdienstes entsteht so ein Ausfall weiterer Services.
Um solche Szenarien beherrschbar zu machen, reicht es nicht aus, ausschließlich die Geschäftslogik zu implementieren. Moderne Anwendungen müssen Fehler erkennen, isolieren und kontrolliert behandeln können. Diese Fähigkeit wird als Resilienz bezeichnet.
Eine etablierte Bibliothek zur Umsetzung von Resilienz-Patterns in Java-Anwendungen ist Resilience4j. Sie unterstützt dabei, Anwendungen widerstandsfähiger gegenüber temporären Fehlern, Überlastung und Ausfällen zu gestalten.
Was ist Resilience4j?
Resilience4j ist eine leichtgewichtige Bibliothek für Fehlertoleranz, die speziell für Java-Anwendungen entwickelt wurde. Sie ermöglicht es, bestehende Geschäftslogik um Resilienz-Patterns zu ergänzen, ohne dafür eine komplexe zusätzliche Infrastruktur einführen zu müssen.
Die Bibliothek stellt eigenständige Module für verschiedene etablierte Resilienz-Patterns bereit:
- Circuit Breaker
- Bulkhead
- Retry
- Rate Limiter
- TimeLimiter
- Cache
Im Folgenden werden die wichtigsten dieser Resilienz-Patterns, ihre Funktionsweise und ihre Einsatzmöglichkeiten genauer betrachtet.
Typische Einsatzgebiete und Grenzen
Resilience4j eignet sich besonders für Aufrufe und Ressourcen, deren Laufzeit, Verfügbarkeit oder Auslastung nicht vollständig unter der Kontrolle des eigenen Prozesses stehen. Typische Einsatzgebiete sind beispielsweise:
- REST- und SOAP-Schnittstellen
- Datenbanken, Messaging-Systeme und verteilte Caches
- Cloud- und SaaS-APIs mit Quotas
- rechenintensive lokale Komponenten
- Legacy-Systeme mit begrenzter Parallelität
- synchrone Integrationen zwischen Microservices
Die Resilienz-Patterns sind dabei nicht auf Microservices beschränkt. Auch in einem modularen Monolithen können sie sinnvoll eingesetzt werden.
Resilience4j ist jedoch kein Ersatz für grundlegende Architektur- und Infrastrukturmaßnahmen wie HTTP-Client-Timeouts, eine saubere Fehlerbehandlung, Load Balancing, Backpressure, Idempotenz oder Kapazitätsplanung. Vielmehr ist die Bibliothek ein Baustein innerhalb einer umfassenderen Resilienzstrategie.
Im Folgenden betrachten wir die wichtigsten Resilienz-Patterns von Resilience4j genauer und zeigen, wie sie funktionieren, konfiguriert und in der Praxis eingesetzt werden können.
Circuit Breaker
Der Circuit Breaker funktioniert analog zu einem elektrischen Schutzschalter. Solange der aufgerufene Service ausreichend zuverlässig antwortet, lässt er Aufrufe durch. Überschreiten Fehler oder langsame Antworten definierte Schwellenwerte, öffnet er den Schaltkreis. Nachfolgende Aufrufe werden dann sofort abgewiesen, anstatt Threads, Connections und Zeit für sehr wahrscheinlich erfolglose Versuche zu verbrauchen.
Typische Anwendungsfälle sind instabile Fremdsysteme, überlastete Downstream-Services oder Schnittstellen, bei denen eine schnelle Ersatzreaktion sinnvoller ist als langes Warten.
Ein Circuit Breaker überwacht die Ergebnisse ausgeführter Aufrufe und wechselt abhängig von den konfigurierten Schwellenwerten zwischen drei Hauptzuständen:
- CLOSED – Normalbetrieb: Aufrufe werden ausgeführt und ihre Ergebnisse im Sliding Window erfasst.
- OPEN – Blockiert: Aufrufe werden nicht mehr ausgeführt. Resilience4j wirft unmittelbar eine
CallNotPermittedException. - HALF_OPEN – Testphase: Nach einer definierten Wartezeit wird eine begrenzte Anzahl von Testaufrufen zugelassen. Sind Fehler- und Slow-Call-Rate wieder akzeptabel, wechselt der Circuit Breaker zurück zu
CLOSED. Andernfalls wechselt er erneut zuOPEN.
Der Circuit Breaker wertet entweder ein count-basiertes Fenster der letzten N Aufrufe oder ein zeitbasiertes Fenster der letzten N Sekunden aus. Eine Bewertung der Fehler- und Slow-Call-Rate erfolgt erst, wenn mindestens minimum-number-of-calls Aufrufe erfasst wurden.
Wichtige Konfigurationsparameter sind:
failure-rate-threshold: Schwellenwert der Fehlerrate in Prozentslow-call-rate-threshold: Schwellenwert für den Anteil langsamer Aufrufeslow-call-duration-threshold: Dauer, ab der ein Aufruf als langsam giltsliding-window-size: Größe des count- oder zeitbasierten Sliding Windowswait-duration-in-open-state: Wartezeit imOPEN-Zustand vor einem möglichen Wechsel zuHALF_OPENpermitted-number-of-calls-in-half-open-state: Anzahl der Testaufrufe imHALF_OPEN-Zustand
Der Circuit Breaker lässt sich mit Resilience4j unkompliziert über die @CircuitBreaker-Annotation in eine bestehende Methode integrieren. Das folgende Beispiel zeigt die dafür notwendigen Anpassungen einschließlich einer Fallback-Methode.
@Service
public class ProductService {
private static final String PRODUCT_API = "productApi";
private final ProductApiClient client;
public ProductService(ProductApiClient client) {
this.client = client;
}
@CircuitBreaker(name = PRODUCT_API, fallbackMethod = "productFallback")
public Product getProduct(String productId) {
return client.loadProduct(productId);
}
private Product productFallback(String productId, Exception ex) {
return Product.temporarilyUnavailable(productId);
}
}
Die zugehörige Konfiguration kann beispielsweise so aussehen:
resilience4j:
circuitbreaker:
instances:
productApi:
sliding-window-type: COUNT_BASED
sliding-window-size: 20
minimum-number-of-calls: 10
failure-rate-threshold: 50
slow-call-duration-threshold: 2s
slow-call-rate-threshold: 60
wait-duration-in-open-state: 30s
permitted-number-of-calls-in-half-open-state: 5
automatic-transition-from-open-to-half-open-enabled: true
record-exceptions:
- org.springframework.web.client.HttpServerErrorException
- org.springframework.web.client.ResourceAccessException
ignore-exceptions:
- com.example.product.ProductNotFoundException
Die gewählten Werte sind lediglich Startpunkte und keine universell gültigen Defaults. Sie sollten anhand der eigenen Service Level Objectives (SLOs) – also konkreten Zielwerten für Verfügbarkeit, Fehlerrate und Antwortzeiten – sowie auf Basis realer Latenzverteilungen und typischer Fehlerbilder festgelegt werden.
Ebenso wichtig ist die Unterscheidung zwischen technischen und fachlichen Fehlern: Eine fachlich erwartete Antwort (wie ein nicht gefundenes Produkt) ist keine technische Störung und darf nicht dazu führen, dass der Circuit Breaker auslöst.
Bulkhead
Bulkhead dient dazu, Ressourcen voneinander zu isolieren und dadurch die Auswirkungen von Überlastung oder langsamen Services zu begrenzen.
Der Name stammt aus dem Schiffsbau: Ein Schiff ist durch Schottwände in mehrere Bereiche unterteilt. Dringt in einem Bereich Wasser ein, bleibt der Schaden möglichst auf dieses Abteil begrenzt und gefährdet nicht unmittelbar das gesamte Schiff.
Nach demselben Prinzip verhindert ein Bulkhead in Software, dass ein einzelner langsamer oder überlasteter Service sämtliche verfügbaren Threads oder Ausführungskapazitäten belegt. Dadurch bleibt ein Teil der Ressourcen für andere Funktionen oder Services verfügbar.
Ein Bulkhead ist insbesondere dann sinnvoll, wenn mehrere Downstream-Services dieselben Ressourcen einer Anwendung nutzen oder wenn eine aufwendige Funktion nur begrenzt parallel ausgeführt werden darf.
Resilience4j stellt dafür zwei Varianten bereit: den SemaphoreBulkhead und den ThreadPoolBulkhead.
Der SemaphoreBulkhead begrenzt die Anzahl gleichzeitig ausgeführter Aufrufe. Bis zu max-concurrent-calls Aufrufe dürfen parallel ausgeführt werden. Ist diese Grenze erreicht, warten weitere Aufrufe höchstens für die mit max-wait-duration konfigurierte Zeit. Steht innerhalb dieses Zeitraums keine Kapazität zur Verfügung, wird eine BulkheadFullException ausgelöst.
Der SemaphoreBulkhead verursacht nur wenig zusätzlichen Overhead und benötigt keinen Wechsel in einen separaten Thread. Er eignet sich daher besonders für synchrone und vergleichsweise kurze Aufrufe. Die eigentliche Verarbeitung findet jedoch weiterhin im Thread des Aufrufers statt. Blockiert ein externer Aufruf, bleibt dadurch auch dieser Thread blockiert.
Die folgende Implementierung zeigt den Einsatz eines SemaphoreBulkheads in einer Spring-Boot-Anwendung:
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping("/{productId}")
@Bulkhead(
name = "productApi",
type = Bulkhead.Type.SEMAPHORE,
fallbackMethod = "bulkheadFallback"
)
public ResponseEntity<Product> getProduct(@PathVariable String productId) {
return ResponseEntity.ok(
productService.getProduct(productId)
);
}
private ResponseEntity<Product> bulkheadFallback(
String productId,
BulkheadFullException ex
) {
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(Product.busy(productId));
}
}
Die zugehörige Konfiguration kann beispielsweise so aussehen:
resilience4j:
bulkhead:
instances:
productApi:
max-concurrent-calls: 30
max-wait-duration: 0
Mit max-wait-duration: 0 folgt die Konfiguration dem Fail-fast-Prinzip. Ist die maximale Anzahl paralleler Aufrufe erreicht, wartet ein weiterer Aufruf nicht auf freie Kapazität, sondern wird unmittelbar abgewiesen. Dadurch wird verhindert, dass sich zusätzlich eine Warteschlange in den aufrufenden Threads aufbaut.
Der ThreadPoolBulkhead geht einen Schritt weiter und führt Aufrufe in einem eigenen, begrenzten Thread-Pool aus. Zusätzlich kann die Anzahl wartender Aufgaben durch eine Queue begrenzt werden. Dadurch entsteht eine stärkere Isolation gegenüber langsamen oder blockierenden Services. Diese Isolation verursacht jedoch zusätzlichen Aufwand durch Thread-Wechsel, Queueing und die Konfiguration eines separaten Thread-Pools.
Ein Beispiel für einen ThreadPoolBulkhead sieht folgendermaßen aus:
@Bulkhead(
name = "productApi",
type = Bulkhead.Type.THREADPOOL,
fallbackMethod = "productFallback"
)
public CompletableFuture<Product> getProduct(String productId) {
return CompletableFuture.supplyAsync(() -> client.loadProduct(productId));
}
private CompletableFuture<Product> productFallback(String productId, BulkheadFullException ex) {
return CompletableFuture.completedFuture(Product.temporarilyUnavailable(productId));
}
Im Gegensatz zum SemaphoreBulkhead wird die Verarbeitung hier über einen separaten Thread-Pool isoliert. Die Anzahl gleichzeitig ausgeführter Aufgaben wird durch die Größe des Thread-Pools begrenzt. Zusätzlich kann die Queue die Anzahl wartender Aufgaben begrenzen.
Die Konfiguration des zugehörigen Thread-Pools kann beispielsweise folgendermaßen erfolgen:
resilience4j:
thread-pool-bulkhead:
instances:
productApi:
core-thread-pool-size: 8
max-thread-pool-size: 12
queue-capacity: 20
keep-alive-duration: 20ms
Die Größe des Thread-Pools sollte dabei nicht isoliert betrachtet werden. Sie muss zu anderen begrenzten Ressourcen wie dem HTTP-Connection-Pool, dem Datenbank-Connection-Pool und der Kapazität des aufgerufenen Systems passen.
Ein Thread-Pool mit 50 Threads bringt beispielsweise keinen zusätzlichen Durchsatz, wenn gleichzeitig nur zehn HTTP-Verbindungen zur Verfügung stehen.
Bei einem Thread-Wechsel sollte außerdem berücksichtigt werden, ob Kontextinformationen wie MDC, SecurityContext, Tracing-Informationen oder andere ThreadLocal-Werte korrekt in den neuen Thread übertragen werden.
Retry
Ein Retry wiederholt einen fehlgeschlagenen Aufruf automatisch. Das ist vor allem bei kurzzeitigen Störungen sinnvoll, zum Beispiel bei einem Netzwerkfehler oder wenn ein Service während eines Deployments vorübergehend nicht erreichbar ist. Bei dauerhaften oder fachlichen Fehlern hilft ein weiterer Versuch dagegen nicht – eine ungültige Anfrage wird auch beim zweiten oder dritten Versuch nicht erfolgreich.
Zwischen den Versuchen sollte eine kurze Wartezeit liegen. Mit exponentiellem Backoff wird diese Wartezeit nach jedem Fehlversuch größer. Zusätzlich kann Jitter verwendet werden, um die Wartezeit leicht zufällig zu verändern. Dadurch verhindern wir, dass viele Instanzen gleichzeitig erneut auf ein bereits belastetes System zugreifen.
Retries sollten außerdem bevorzugt bei idempotenten Operationen eingesetzt werden. Bei schreibenden Aufrufen muss sichergestellt werden, dass eine Wiederholung nicht versehentlich dieselbe Aktion mehrfach ausführt.
So könnte eine Retry-Konfiguration mit exponentiellem Backoff und Jitter umgesetzt werden:
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductClient productClient;
@Retry(name = "productApi")
public Product getProduct(String productId) {
return productClient.getProduct(productId);
}
}
Die Retry-Strategie kommt dann vollständig aus der Konfiguration:
resilience4j:
retry:
instances:
productApi:
max-attempts: 3
wait-duration: 200ms
enable-exponential-backoff: true
exponential-backoff-multiplier: 2
exponential-max-wait-duration: 2s
enable-randomized-wait: true
randomized-wait-factor: 0.5
retry-exceptions:
- org.springframework.web.client.HttpServerErrorException
- org.springframework.web.client.ResourceAccessException
ignore-exceptions:
- com.example.product.ProductNotFoundException
Rate Limiter
Ein Rate Limiter begrenzt, wie viele Aufrufe innerhalb eines definierten Zeitfensters erlaubt sind. Dadurch kann verhindert werden, dass ein externes System oder die eigene Anwendung durch eine zu hohe Aufrufrate belastet wird. Im Gegensatz zum Bulkhead begrenzt ein Rate Limiter dabei nicht die Anzahl der gleichzeitig laufenden Aufrufe, sondern die Anzahl der Aufrufe innerhalb eines Zeitfensters. Ein Bulkhead kontrolliert dagegen die maximale Anzahl parallel ausgeführter Aufrufe.
Typische Anwendungsfälle sind externe APIs mit festen Request-Limits, rechenintensive Operationen oder ältere Systeme, die nur eine begrenzte Anzahl von Anfragen verarbeiten können.
Ist das konfigurierte Limit erreicht, kann ein Aufruf entweder eine bestimmte Zeit warten oder sofort abgewiesen werden. Resilience4j signalisiert eine solche Ablehnung mit einer RequestNotPermitted.
Eine mögliche Umsetzung eines Rate Limiters für eine externe Pricing-API sieht folgendermaßen aus:
@RateLimiter(
name = "productApi",
fallbackMethod = "rateLimitFallback"
)
public Product getProduct(String productId) {
return productClient.getProduct(productId);
}
private Product rateLimitFallback(
String productId,
RequestNotPermitted ex
) {
return Product.temporarilyUnavailable(productId);
}
Und entsprechend die Konfigurationen:
resilience4j:
ratelimiter:
instances:
productApi:
limit-for-period: 20
limit-refresh-period: 1s
timeout-duration: 0
limit-for-period: 20 erlaubt maximal 20 Aufrufe pro Zeitfenster. Mit limit-refresh-period: 1s wird dieses Limit jede Sekunde erneuert. timeout-duration: 0 bedeutet, dass weitere Aufrufe nicht warten, sondern sofort mit RequestNotPermitted abgewiesen werden.
Wichtig ist, dass das Limit pro Instanz gilt und nicht für alle Pods zusammen. Wenn die Anwendung beispielsweise mit zwei Pods läuft und pro Pod 20 Aufrufe pro Sekunde erlaubt sind, kann die externe API insgesamt mit bis zu 40 Aufrufen pro Sekunde angesprochen werden.
Time Limiter
Ein langsamer Service kann für eine Anwendung genauso problematisch sein wie ein ausgefallener Service. Wartet ein Aufruf zu lange auf eine Antwort, bleiben beispielsweise Threads und Verbindungen unnötig belegt. Bei vielen solchen Aufrufen können dadurch schnell Engpässe entstehen.
Der TimeLimiter begrenzt deshalb, wie lange auf das Ergebnis einer Operation gewartet wird. Wird die konfigurierte Zeit überschritten, schlägt der Aufruf mit einer TimeoutException fehl. Resilience4j unterstützt dafür unter anderem CompletionStage und Future.
So lässt sich die TimeLimiter-Anmerkung für einen asynchronen Methodenaufruf in einer Spring-Boot-Anwendung integrieren:
@TimeLimiter(
name = "productApi",
fallbackMethod = "timeoutFallback"
)
public CompletableFuture<Product> getProduct(String productId) {
return CompletableFuture.supplyAsync(
() -> productClient.getProduct(productId),
productExecutor
);
}
private CompletableFuture<Product> timeoutFallback(
String productId,
TimeoutException ex
) {
return CompletableFuture.completedFuture(
Product.temporarilyUnavailable(productId)
);
}
Die dazugehörige YAML-Konfiguration sieht folgendermaßen aus:
resilience4j:
timelimiter:
instances:
productApi:
timeout-duration: 2s
cancel-running-future: true
timeout-duration: 2s begrenzt die maximale Wartezeit auf zwei Sekunden.
cancel-running-future: true versucht, ein noch laufendes Future nach einem Timeout abzubrechen.
Cache
Neben den Resilienz-Patterns bietet Resilience4j auch ein Cache-Modul an. Da Spring Framework mit der @Cacheable-Abstraktion bereits einen umfassenden Standard für Caching mitbringt, wird das Cache-Modul in diesem Beitrag nicht weiter vertieft.
Patterns im Zusammenspiel
Die einzelnen Resilienz-Patterns lösen jeweils unterschiedliche Probleme. In der Praxis werden sie jedoch häufig kombiniert, um einen externen Aufruf gegen mehrere Fehlerquellen gleichzeitig abzusichern. Dabei ist nicht nur entscheidend, welche Patterns eingesetzt werden, sondern auch in welcher Reihenfolge sie ausgeführt werden.
Bei der Verwendung von Spring-Annotationen wie @Retry, @CircuitBreaker oder @RateLimiter ergibt sich die tatsächliche Ausführungsreihenfolge nicht einfach aus der Reihenfolge der Annotationen im Code. Sie wird durch die Resilience4j-Aspects bestimmt.
Die Standardreihenfolge der Resilience4j-Aspects ist:
Retry → CircuitBreaker → RateLimiter → TimeLimiter → Bulkhead → Function
Damit liegt beispielsweise der Retry außen und der Bulkhead näher am eigentlichen Methodenaufruf. Ein fehlgeschlagener Aufruf kann dadurch zunächst vom Circuit Breaker bewertet und anschließend vom Retry erneut ausgeführt werden. Die einzelnen Patterns wirken also nicht unabhängig voneinander, sondern bilden eine verschachtelte Ausführungskette.
Soll eine andere Reihenfolge verwendet werden, kann die Aspect-Reihenfolge über die entsprechende Konfiguration angepasst werden. Bei komplexeren Kombinationen kann außerdem eine funktionale Verkettung mit Decorators übersichtlicher sein, da dort direkt im Code sichtbar wird, welches Pattern ein anderes umschließt und in welcher Reihenfolge die Aufrufe tatsächlich ausgeführt werden.
Supplier<Product> backendCall = () -> client.loadProduct(productId);
Supplier<Product> protectedCall = Decorators.ofSupplier(backendCall)
.withBulkhead(bulkhead)
.withRateLimiter(rateLimiter)
.withCircuitBreaker(circuitBreaker)
.withRetry(retry)
.decorate();
return protectedCall.get();
In diesem Beispiel wird der eigentliche Backend-Aufruf zunächst durch den Bulkhead geschützt. Anschließend folgen Rate Limiter, Circuit Breaker und schließlich Retry.
Die Reihenfolge der Resilienz-Patterns hängt davon ab, welches Verhalten Sie schützen oder bewerten wollen:
- RateLimiter & Bulkhead: Nah am Backend-Aufruf platzieren. So greift der Schutz bei jedem Retry-Versuch direkt und verhindert, dass Limits umgangen oder Ressourcen unweigerlich blockiert werden.
- Retry & CircuitBreaker:
- Retry außen: Der Circuit Breaker bewertet jeden einzelnen Versuch.
- Circuit Breaker außen: Er bewertet nur das Gesamtergebnis aller Retries.
Faustregel: Schutz-Patterns möglichst nah an der zu schützenden Ressource platzieren und bewusst entscheiden, ob einzelne Versuche oder der gesamte Aufruf bewertet werden sollen.
Anti-Patterns
Resilience4j kann die Stabilität eines Systems deutlich verbessern. Falsch eingesetzt können die Patterns jedoch zusätzliche Last erzeugen oder Fehler sogar verstärken. Einige typische Anti-Patterns sollte man daher vermeiden.
Zu viele Patterns gleichzeitig einsetzen: Nicht jeder Aufruf benötigt Retry, CircuitBreaker, Bulkhead, RateLimiter und TimeLimiter. Jedes Pattern sollte ein konkretes Problem lösen. Eine unnötig komplexe Kombination erschwert Konfiguration, Fehlersuche und Monitoring.
Retry bei nicht wiederholbaren Fehlern: Fachliche Fehler wie ungültige Eingaben oder ein 404 Not Found verschwinden normalerweise nicht durch einen weiteren Versuch. Retries sollten hauptsächlich bei temporären Fehlern eingesetzt werden. Besonders bei nicht-idempotenten Operationen muss außerdem verhindert werden, dass eine Aktion mehrfach ausgeführt wird.
Timeouts nicht aufeinander abstimmen: HTTP-, Connection- und TimeLimiter-Timeouts sollten zu einem gemeinsamen End-to-End-Zeitbudget passen. Andernfalls können Aufrufe im Hintergrund weiterlaufen, obwohl der aufrufende Service bereits aufgegeben hat.
Fehlender oder zu komplexer Fallback: Ein Fallback sollte möglichst einfach, schnell und von wenigen weiteren Abhängigkeiten abhängig sein. Ruft er selbst wieder eine Datenbank oder einen externen Dienst auf, kann auch dieser Aufruf fehlschlagen und das ursprüngliche Problem zusätzlich verstärken.
Kein Exponential Backoff bei Retries: Werden fehlgeschlagene Aufrufe ohne Verzögerung direkt wiederholt, kann ein bereits überlasteter Service zusätzlich belastet werden. Ein exponentieller Backoff, idealerweise mit Jitter, verteilt die Retry-Versuche zeitlich und reduziert das Risiko eines Retry Storms.
Monitoring
Resilienz-Patterns sollten im Betrieb überwacht werden, um ungeeignete Grenzwerte oder häufige Fehler frühzeitig zu erkennen. Resilience4j stellt dafür über Micrometer Metriken bereit, die sich mit Spring Boot Actuator sowie beispielsweise Prometheus und Grafana auswerten lassen.
Relevant sind unter anderem Circuit-Breaker-Zustände, Retry-Anzahlen, Bulkhead-Auslastung, Rate-Limiter-Ablehnungen und Timeouts. Die Konfiguration sollte regelmäßig anhand dieser Produktionsdaten überprüft und bei Bedarf angepasst werden.
Fazit
Resilience4j entfaltet seinen Nutzen vor allem dann, wenn die einzelnen Patterns gezielt und mit einem klaren Zweck eingesetzt werden. Wichtig ist nicht nur die Auswahl eines geeigneten Mechanismus, sondern auch dessen Zusammenspiel mit anderen Patterns und die passende Konfiguration für den jeweiligen Anwendungsfall.
Mit einer durchdachten Kombination lassen sich externe Abhängigkeiten besser absichern, Ressourcen schützen und Fehler kontrollierter behandeln. Dabei sollte die Konfiguration regelmäßig durch Tests, Metriken und reale Produktionsdaten überprüft und weiterentwickelt werden.