Circuit Breaker : Resilience4J

ümit Samimi
4 min readJan 28, 2023

Çalıştığım kurumdaki microservis uygulamalarından bir tanesinde circuit breaker için Resilience4J’yi tercih ettik. İlgili dokümantasyonu ve configürasyonu Refik Urhan arkadaşımız yaptı. Ben de onun sağladığı notları bir araya getirip, medium üzerinden paylaşmak istedim.

Öncelikle belirtmek isterim ki buradaki örnek başlangıç seviyesindedir

Circuit Breaker aslında bir desen(pattern) adıdır. Uygulamanızda belli durumlarda karşılaşabileceğiniz yani önceden tahmin ettiğiniz durumlara karşı aldığınız önlemler kümesi gibi düşünebilirsiniz. Örneğin X servisi Y servisine her gittiğinde hata alıyor. Hala gitmeye devam etmesine gerçekten gerek var mı?

Bu ve benzeri durumlar için bazı konfigürasyonlar yapılır. Belirtilen koşullarda Y servisine gitmek yerine varsayılan(default) yanıt ile dönüş sağlanabilir.

Resilience4J

Resilience4J için CircuitBreaker’ın 3 farklı durumu(state) olabilir

  • CLOSED : Herhangi bir sorunun yaşanmadığı için devrenin kapalı olması durumu
  • OPEN : Sorunun yaşanması ile birlikte devrenin açılması durumu
  • HALF- OPEN : Devre açık(open) durumundan belirlenen süre sonrasında yarı açık duruma geçerek sorunun devam edip etmediğini kontrol ettiği durumdur.

Haydi kodlayalım.

Öncelikle projemize Resilience4j’nin spring ile çalışan bağımlılığını eklememiz gerekiyor.

<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</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>

Yukarıda Y olarak isimlendirdiğimiz servisin başına aşağıdaki anatasyonu ekleyelim.

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;

@CircuitBreaker(name = "getCustomerCircuitBreaker",
fallbackMethod = "getCustomerCircuitBreakerFallback")
public CustomerResponse getCustomer(String customerNo) {
//...
}

Devre açık iken varsayılan olarak servis ne dönmeli? Bu bizim tanımımıza göre değişir. Bunun için bir Fallback method tanımlamalıyız.

private CustomerResponse getCustomerCircuitBreakerFallback(String param, Exception e) {
log.warn("Circuit breaker fallback method");
return new CustomerResponse();
}

Son olarak konfigürasyon dosyamıza özellikleri tanımlamalıyız.

resillience4j:
circuitbreaker:
instances:
getCustomerByCustomerNo:
failureRateThreshold: 50
slowCallRateThreshold: 100
slowCallDurationThreshold: 3000
permittedNumberOfCallsInHalfOpenState: 10
minimumNumberOfCalls: 50
waitDurationInOpenState: 5s
automaticTransitionFromOpenToHalfOpenEnabled: false

management:
health:
circuitbreakers:
enabled: true
  • failureRateThreshold: Hata alan çağrıların yüzde olarak limitini belirlemek için bu değer kullanılır.
  • slowCallRateThreshold: Yavaş çağrıların yüzde olarak limitini belirlemek için bu değer kullanılır.
  • slowCallDurationThreshold: Bir çağrının yavaş olarak nitelendirilebilmesi için geçmesi gereken süre.
  • permittedNumberOfCallsInHalfOpenState: CB HALF_OPEN durumdayken kaç çağrıya izin verileceği bu değer ile belirlenir.
  • slidingWindowType: Yapılan çağrıların ölçümünde kullanılacak slidingWindow tipini belirler. COUNT_BASED: son “x” çağrı için TIME_BASED: son “x” saniyede yapılan çağrılar için default değeri: COUNT_BASED
  • slidingWindowSize: slidingWindow’un büyüklüğü. Yukarıdaki maddede “x” yerine bu değer yazılıyor.
  • minimumNumberOfCalls: CB’nin hata ya da slow call oranını ölçmeye başlaması için yapılması gereken minimum çağrı sayısı.
  • waitDurationInOpenState: CB’nin OPEN durumdan HALF_OPEN duruma geçmeden önce beklemesi gereken süre

Retry

Eğer Retry mekanizması kurmak istiyorsanız aşağıdaki methodu da kullanabilirsiniz

import io.github.resilience4j.retry.annotation.Retry;

@Retry(name = "getCustomerRetry", fallbackMethod = "fallbackRetry")
public CustomerResponse getCustomerRetry(String customerNo) {
//....
}

Tabi öncesinde retry için ilgili konfigürasyonu yapmanız gerekmektedir

retry:
instances:
getCustomerRetry:
maxAttempts: 3
waitDuration: 10s
failAfterMaxAttempts: true
enable-exponential-backoff: true
exponential-backoff-multiplier: 2
  • maxAttempts: max retry sayısı. (ilk çağrı dahil) : default: 3
  • waitDuration: iki retry arasındaki bekleme süresi : default: 500 ms
  • intervalFunction: retry interval’i değiştirmek için yazılabilecek bir fonksiyon. İlk retry 1 saniye, sonraki 3 saniye, sonraki 5 saniye gibi aralıklar belirlemek için bu property kullanılabilir.
  • retryOnResultPredicate: Çağrının retry edilip edilmeyeceği burada kullanılan bir predicate ile belirlenebilir. Retry edilmesi gerekiyorsa true, edilmemesi gerekiyorsa false dönmelidir.
  • retryExceptionPredicate: Alınan exception’a göre çağrının retry edilmesi gerekiyorsa, bu predicate ile karar verilebilir. Retry edilmesi gerekiyorsa true, edilmemesi gerekiyorsa false dönmelidir.
  • retryExceptions: retry edilecek exception listesi bu property içinde tutulabilir.
  • ignoreExceptions: retry noktasında ignore edilecek exception listesi bu property içinde tutulabilir.
  • failAfterMaxAttempts: maxAttempts’e erişildikten sonra MaxRetriesExceededException fırlatılıp fırlatılmayacağının kararı bu property ile verilir.
  • enable-exponential-backoff: iki retry arasında beklenen sürenin exponential olarak değişip değişmeyeceğine bu property ile karar verilir.
    exponential-backoff-multiplier: exponential-backoff kuvveti (sürenin ne kadar hızlı artacağı)
wait-duration * (exponential-backoff-multiplier ^ (retry iteration count))

yukarıdaki configuration’a göre;

  • ilk retry: 5s * 2⁰ = 5 saniye sonra
  • ikinci retry: 5s * 2¹ = 10 saniye sonra
  • üçüncü retry: 5s * 2² = 20 saniye sonr

RateLimiter

Bir servise gelen çağrı sayısını sınırlamak isterseniz eğer, RateLimiter kullanabilirsiniz. Genellikle Bulkhead ile karıştırılır.

  • RateLimiter, bir süre boyunca alabileceği çağrı sayısını limitler
  • Bulkhead, paralel olarak yapılacak çağrıları limitlemek için kullanılır

Örneğin Bulkhead, paralel olarak 5 çağrıyı aynı anda alabilir. Fakat
RateLimiter, her iki saniye için 5 çağrı al şeklinde çalışır.

Bulkhead’da 5 çağrı paralel olarak işlenir. Çağrıların birisi tamamlandığı anda, yeni
çağrı işlenebilir

RateLimiter için, 2 saniyelik bir periyotta 5 çağrı alınır. İstekler, 2 saniyeden önce
tamamlansa bile, yeni istek alınmaz, periyodun tamamlanması beklenir

Eğer RateLimiter kullanmak isterseniz, aşağıdaki yöntemi takip edebilirsiniz

import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
@RateLimiter(name = "getCustomerRateLimiter", fallbackMethod = "fallbackRateLimiter")
public CustomerResponse getCustomerRateLimiter(String customerNo) {
//...
}

Tabi öncesinde rate limiter için ilgili konfigürasyonu yapmanız gerekmektedir

ratelimiter:
instances:
getCustomerRateLimiter:
timeoutDuration: 5
limitRefreshPeriod: 500s
limitForPeriod: 32

TimeLimiter

Çağrının belli bir süreden sonra timeout’a düşmesi için kullanılır.

Time limiter kullanmak isterseniz, methodunuzun imzasını CompletableFuture ile değiştirmeniz gerekmektedir.

import io.github.resilience4j.timelimiter.annotation.TimeLimiter;

@TimeLimiter(name = "getCustomerTimeLimiter", fallbackMethod = "fallbackTimeLimiter")
public CompletableFuture<CustomerResponse> getCustomerTimeLimiter(String customerNo) {
return CompletableFuture.supplyAsync(() -> {
// ..
});
}

Tabi öncesinde timelimiter için ilgili konfigürasyonu yapmanız gerekmektedir

timelimiter:
instances:
getCustomerTimeLimiter:
timeoutDuration: 3s
cancelRunningFuture: true
  • timeoutDuration: uygulamanın kaç saniye sonra timeout’a düşeceği.
  • cancelRunningFuture: çalışan async operasyonun durdurulup durdurulmayacağı.

Bulkhead

Concurrent servis çağrılarını limitlemek için Bulkhead kullanmak isterseniz, aşağıdaki yöntemi takip edebilirsiniz.

@Bulkhead(name = "getCustomerBulkhead", fallbackMethod = "fallbackBulkhead", type = Bulkhead.Type.SEMAPHORE)
public CustomerResponse getCustomerBulkhead(String customerNo) {
//...
}

Bulkhead’in iki farklı yöntemi mevcuttur.

  • SemaphoreBulkhead
  • FixedThreadPoolBulkhead

Tabi öncesinde bulkhead için ilgili konfigürasyonu yapmanız gerekmektedir

bulkhead:
instances:
getCustomerBulkhead:
maxConcurrentCalls: 1
maxWaitDuration: 200
  • maxConcurrentCalls: ilgili servisin paralel kaç çağrı alabileceğini ifade eder. Default değeri 25'tir.
  • maxWaitDuration : Dolu bulkhead’e gelen çağrının exception fırlatmadan önce kaç ms bekleyeceğini belirler. Default değeri 0 ms’dir.

Umarım faydalı olmuştur.

--

--