in

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.

Circuit breaker'ın kapalı, açık ve yarı açık durumları arasındaki geçişleri gösteren durum diyagramı

Ü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; minimumNumberOfCalls tek 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

  1. Idempotent olmayan POST'a retry. Sipariş oluşturma tekrar denenirse çift sipariş; retry yalnızca idempotent çağrılara ya da idempotency anahtarıyla.

  2. İç içe retry. HTTP istemcisinde 3, Resilience4j'de 3, çağıran serviste 3 deneme: 27 istek. Retry tek katmanda olmalı.

  3. 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.

  4. İş hatalarını arıza saymak. "Ürün bulunamadı" (404) devreyi açmamalı; ignoreExceptions ile ayrılır.

  5. 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.

  6. Test edilmemiş fallback. Fallback ilk kez üretimde çalışır ve NullPointerException verir; 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.