Resilience4j ile Circuit Breaker, Retry ve Bulkhead
Yavaşlayan bir dış servis, thread havuzunu doldurup tüm uygulamayı düşürebilir; dayanıklılık desenleri bu zinciri kırar. Resilience4j'nin circuit breaker, retry, bulkhead, time limiter ve rate limiter modüllerini, üretimde işe yarayan yapılandırma değerlerini, fallback stratejilerini ve sık yapılan hataları Spring Boot örnekleriyle anlatıyoruz.
Üretimde en yıkıcı arıza türü, arızanın kendisi değil yayılmasıdır: ERP'nin stok servisi yavaşlar, her istek 30 saniye bekler, Tomcat'in 200 thread'i bekleyen isteklerle dolar ve stokla ilgisi olmayan giriş ekranı bile açılmaz olur. Buna zincirleme arıza (cascading failure) denir ve çözümü dış servisi hızlandırmak değil, ona olan bağımlılığı kontrollü biçimde kesmektir.
Resilience4j bu kontrolün Java'daki standart araç setidir. Bu yazıda beş modülünü — circuit breaker, retry, time limiter, bulkhead ve rate limiter — ne zaman hangisinin gerektiğini, Spring Boot'ta kurulumu, üretimde işe yarayan yapılandırma değerlerini, fallback stratejilerini, ölçümü ve sahada sık yapılan hataları anlatıyoruz.
Beş modül, beş farklı soru
Modül | Cevapladığı soru | Mekanizma | Tipik kullanım |
|---|---|---|---|
TimeLimiter | Ne kadar beklerim? | Süre aşımında çağrıyı iptal eder | Her dış çağrı; çağıranın kendi zaman aşımından kısa |
Retry | Geçici hatada tekrar denesem düzelir mi? | Üstel geri çekilme ve jitter ile sınırlı tekrar | Ağ hatası, 503, kilit çakışması; yalnızca idempotent işlemler |
CircuitBreaker | Karşı taraf hasta mı? Denemeyi bırakayım mı? | Hata/yavaşlık oranı eşiği aşınca çağrıları hızlıca reddeder | Sürekli başarısız olan bağımlılığı izole etmek |
Bulkhead | Bu bağımlılık kaç thread/çağrı işgal edebilir? | Eşzamanlı çağrı sayısını sınırlar | Yavaş bağımlılığın tüm havuzu tüketmesini önlemek |
RateLimiter | Karşı tarafa saniyede kaç istek atabilirim? | Zaman dilimi başına izin sayısı | Kotalı üçüncü taraf API'leri, ERP'yi boğmamak |
Circuit breaker: üç durum
CLOSED (kapalı, normal): çağrılar geçer, sonuçlar kayan pencerede sayılır. Hata oranı ya da yavaş çağrı oranı eşiği aşınca OPEN (açık): çağrılar denenmeden CallNotPermittedException ile anında reddedilir; thread bekletilmez, fallback devreye girer. Bekleme süresi dolunca HALF_OPEN (yarı açık): sınırlı sayıda deneme çağrısı geçer; başarılıysa kapalıya, değilse tekrar açığa döner. Kritik nokta: açık devre bir hata değil, koruma mekanizmasıdır — kullanıcı boş ekran yerine "son bilinen stok" görür, sistem ayakta kalır.
Spring Boot'ta kurulum
<dependency><groupId>io.github.resilience4j</groupId><artifactId>resilience4j-spring-boot3</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>resilience4j:
circuitbreaker:
instances:
sap:
slidingWindowType: COUNT_BASED
slidingWindowSize: 50 # son 50 çağrı
minimumNumberOfCalls: 20 # karar için en az 20 çağrı
failureRateThreshold: 50 # %50 hata → aç
slowCallDurationThreshold: 2s # p99'un ~2 katı
slowCallRateThreshold: 60 # %60 yavaş → aç
waitDurationInOpenState: 30s
permittedNumberOfCallsInHalfOpenState: 5
ignoreExceptions:
- com.acme.erp.sap.SapIsKuraliHatasi # 4xx benzeri iş hataları arıza sayılmaz
retry:
instances:
sap:
maxAttempts: 3
waitDuration: 500ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
enableRandomizedWait: true # jitter: herkes aynı anda tekrar denemesin
retryExceptions:
- java.io.IOException
- org.springframework.web.client.HttpServerErrorException
ignoreExceptions:
- org.springframework.web.client.HttpClientErrorException
timelimiter:
instances:
sap:
timeoutDuration: 3s
cancelRunningFuture: true
bulkhead:
instances:
sap:
maxConcurrentCalls: 25
maxWaitDuration: 100ms@Component
public class SapStokAdaptoru implements StokKaynagi {
private final SapOdataClient client;
private final StokOnbellek onbellek;
@Override
@CircuitBreaker(name = "sap", fallbackMethod = "sonBilinen")
@Retry(name = "sap")
@Bulkhead(name = "sap")
@TimeLimiter(name = "sap")
public CompletableFuture<StokDurumu> sorgula(UrunKodu urun, DepoKodu depo) {
return CompletableFuture.supplyAsync(() -> client.stok(urun, depo), executor)
.thenApply(SapStokEsleme::toStokDurumu);
}
// fallback: aynı dönüş tipi, son parametre Throwable (ya da belirli exception)
CompletableFuture<StokDurumu> sonBilinen(UrunKodu urun, DepoKodu depo, Throwable t) {
return CompletableFuture.completedFuture(
onbellek.sonDeger(urun, depo).orElse(StokDurumu.bilinmiyor(urun, depo)));
}
}Anotasyonların uygulanma sırası varsayılan olarak dıştan içe Retry → CircuitBreaker → RateLimiter → TimeLimiter → Bulkhead şeklindedir: zaman aşımı ve bulkhead reddi bir hata olarak devre kesiciye sayılır, devre kesici açıksa retry tekrar dener (açık devrede denemenin anlamı yoktur; CallNotPermittedException'ı retry'ın ignore listesine ekleyin). @TimeLimiter yalnızca CompletableFuture dönen metotlarda çalışır; senkron istemcilerde zaman aşımı HTTP istemcisinin kendi ayarıyla (bağlantı ve okuma zaman aşımı) verilir.
Fallback: ne döndüreceğinizi baştan tasarlayın
Fallback'in olmadığı devre kesici yalnızca hatanın türünü değiştirir. Dört strateji vardır ve seçim iş kararıdır:
Son bilinen değer: Stok, fiyat, kur gibi okumalarda önbellekten "12 dakika önceki" değer, ekranda "güncel olmayabilir" işaretiyle.
Varsayılan / bozulmuş mod: Öneri servisi yoksa popüler ürünler; kredi skoru yoksa "onaya düşür".
Ertelenmiş işlem: Yazma işlemlerinde isteği outbox'a ya da kuyruğa al, "işleme alındı" de, karşı taraf açılınca gönder.
Hızlı ve açık hata: Ödeme gibi yerine bir şey konamayan işlemlerde kullanıcıya net mesaj, teknik hata değil.
Yapılmaması gereken: fallback'in aynı bağımlılığı başka bir yoldan çağırması (koruma yok olur) ya da hatayı sessizce yutup boş liste döndürmesi (kimse fark etmez).
Yapılandırma değerleri nasıl seçilir?
Zaman aşımı bütçesi: Kullanıcı isteğinin toplam zaman aşımı 10 sn ise içerideki dış çağrı 3 sn, retry ile toplam 3 deneme × 3 sn'yi aşamaz; iç zaman aşımı her zaman dıştakinden kısa olmalıdır.
Yavaş çağrı eşiği: Bağımlılığın normal p99'unun yaklaşık iki katı; ölçmeden "2 saniye" yazmak ya çok erken ya çok geç açar.
Pencere ve minimum çağrı: Düşük trafikli bağımlılıkta zaman tabanlı pencere (
TIME_BASED, 60 sn) daha kararlıdır;minimumNumberOfCallstek bir hatanın devreyi açmasını önler.Retry sayısı: 3 deneme, 500 ms başlangıç, 2× çarpan, jitter açık. Daha fazlası karşı sisteme "retry fırtınası" yaratır.
Bulkhead: Bağımlılığın makul eşzamanlılığı (örneğin ERP için 25) + kısa bekleme; sanal thread kullanılıyorsa semaphore tabanlı bulkhead yeterlidir, thread havuzlu bulkhead gereksizdir.
Ölçüm ve alarm
Resilience4j Micrometer'a otomatik metrik verir: resilience4j.circuitbreaker.state (hangi devre açık), resilience4j.circuitbreaker.calls (başarılı/başarısız/yavaş), resilience4j.retry.calls (kaçıncı denemede başarılı), resilience4j.bulkhead.available.concurrent.calls. /actuator/circuitbreakers ve /actuator/circuitbreakerevents anlık durumu gösterir. Alarm kuralı basittir: bir devre 5 dakikadan uzun açıksa bildirim; retry oranı sürekli yüksekse (isteklerin %20'si ikinci denemede başarılı) bağımlılıkla konuşma zamanıdır.
Sahada sık yapılan hatalar
Idempotent olmayan POST'a retry. Sipariş oluşturma tekrar denenirse çift sipariş; retry yalnızca idempotent çağrılara ya da idempotency anahtarıyla.
İç içe retry. HTTP istemcisinde 3, Resilience4j'de 3, çağıran serviste 3 deneme: 27 istek. Retry tek katmanda olmalı.
Tek devre kesici, tüm bağımlılıklar. ERP'nin stok ucu bozulunca fiyat ucu da kapanır; devre kesici bağımlılık + işlem bazında adlandırılır.
İş hatalarını arıza saymak. "Ürün bulunamadı" (404) devreyi açmamalı;
ignoreExceptionsile ayrılır.Zaman aşımı olmadan devre kesici. Devre kesici hata görmeden açılamaz; 30 saniye bekleyen çağrılar hata değil "başarı" sayılır. Önce zaman aşımı, sonra devre kesici.
Test edilmemiş fallback. Fallback ilk kez üretimde çalışır ve
NullPointerExceptionverir; entegrasyon testinde bağımlılığı bilinçli düşürerek fallback yolu test edilir.
Sık sorulan sorular
Spring Cloud Circuit Breaker mı, doğrudan Resilience4j mi?
Spring Cloud Circuit Breaker, Resilience4j'nin üzerinde soyut bir katmandır ve çoklu sağlayıcı değişimi için tasarlanmıştır. Tek sağlayıcı kullanan çoğu projede doğrudan Resilience4j anotasyonları daha az soyutlama ve daha fazla ayar imkânı verir.
Devre kesici mesaj tüketicilerinde de kullanılır mı?
Evet; Kafka tüketicisi ERP'ye yazamıyorsa devre açılır ve tüketici mesajı işlemeyi durdurup (pause) beklemelidir; mesajları hata kuyruğuna doldurmak yerine devre kapanınca kaldığı yerden devam eder.
Bu desenler mikroservis dışında gerekli mi?
Modüler monolit bile ERP, ödeme, e-fatura ve e-posta gibi dış bağımlılıklara sahiptir; her dış çağrı bir zincirleme arıza adayıdır. Desenler mimariye değil, dış bağımlılığa bağlıdır.
Sonuç
Dayanıklılık, dış servislerin hiç bozulmaması değil, bozulduğunda sisteminizin ne yapacağını önceden kararlaştırmış olmanızdır: zaman aşımı bütçesi, yalnızca idempotent işlerde ölçülü retry, bağımlılık bazında devre kesici, tasarlanmış fallback ve açık devre alarmı. Resilience4j bu kararları birkaç anotasyon ve bir YAML dosyasıyla uygulanabilir kılar.
Mevcut entegrasyonlarınızı zincirleme arızaya karşı dayanıklı hale getirmek için entegrasyon ve otomasyon ile bakım ve destek hizmetlerimize göz atabilirsiniz.