Resilience Pattern: Bulkhead, Rate Limiter, Time Limiter, Retry

Gokhan Konuk
Akbank Teknoloji
Published in
6 min readJun 20, 2023

--

Akbank Teknoloji olarak altyapımızı daha dayanaklı inşa edebilmek için araştırmalarını yapıp ve POC’lerini gerçekleştirdiğimiz “Resilience Pattern” tasarım desenlerini ele aldığımız yazı serisinin ikinci adımında, Bulkhead, Rate Limiter, Time Limiter, Retry konularını ele alacağız.

Mikroservis tabanlı uygulamalar geliştirdiğimizde, bu uygulamaları gerçek zamanlı olarak çalıştırırken bazı sapmalar yaşama ihtimalimiz yüksektir. Slow Response, Network Failures, REST Call Failures, Failures due to High Number of Requests ve çok daha fazlası olabilir. Bu tür şüpheli hataları tolere etmek için uygulamamıza Fault Tolerance mekanizmasını dahil etmemiz gerekir. Bunu başarmak için Resilience4j kütüphanesinden faydalanacağız. Resilience4j, daha önceki yazımızda da bahsettiğimiz gibi Netflix Hystrix’ten ilham alan, Java 8 ve işlevsel programlama için tasarlanmış hafif, kullanımı kolay bir hata toleransı kütüphanesidir. Bu makaledeki odak noktamız bu kütüphaneyi kullanarak Bulkhead, Rate Limiter, Time Limiter ve Retry kullanımlarını göstermektir.

Bulkhead Nedir?

Bulkhead ismi, bir geminin gövdesinin birkaç su geçirmez bölmeye bölündüğü eski bir gemi inşa tekniğinden geliyor. Bölmelerden birinde sızıntı varsa, su tüm gemi yerine yalnızca o bölmeyi doldurur ve kontrol altına alınır. Bu prensibi yazılım uygulamalarına ve mikroservislere de uygulayabiliriz. Arızaları ayrı ayrı bileşenlere yalıtarak, tek bir arızanın art arda gelişerek tüm sistemi çökertmesini önleyebiliriz.
Bölmeler ayrıca herhangi bir arızanın etkisini azaltarak hizmetlerin belirli bir hizmet düzeyini koruyabilmesini sağlayarak Single Points of Failure önlemeye yardımcı olur.

Fault Tolerance mekanizması bağlamında, eşzamanlı isteklerin sayısını sınırlamak istiyorsak, bir özellik olarak Bulkhead’i kullanabiliriz. Bulkhead’i kullanarak, belirli bir süre içinde eşzamanlı isteklerin sayısını sınırlayabiliriz. Lütfen Bulkhead ve Rate Limiter arasındaki farka dikkat edin. Rate Limiter hiçbir zaman eşzamanlı isteklerden bahsetmez, ancak Bulkhead bu konuya odaklanır. Rate Limiter, belirli bir süre içinde istek sayısını sınırlamaktan bahseder. Bu nedenle, Bulkhead’i kullanarak eşzamanlı isteklerin sayısını sınırlayabiliriz.

Bulkhead’i, bir bölmenin hasarını tüm geminin batmasına neden olmayacak şekilde ayıran gemi içindeki bir duvar gibi görebiliriz.

Rate Limiter Nedir?

Rate Limiter, belirli bir aralık için istek sayısını sınırlar. Örneğin; bir Rest API üzerindeki istek sayısını sınırlamak ve belirli bir süre için düzeltmek istediğimizi varsayalım. Bir API’nin işleyebileceği isteklerin sayısını sınırlamak için kaynakları spam gönderenlerden korumak, ek yükü en aza indirmek, bir hizmet düzeyi sağlamak ve daha pek çok neden, Rate Limiter ile çözülebilmektedir.

Time Limiter Nedir?

Time Limiter, bir mikroservisin yanıt vermesi için bir zaman sınırı belirleme işlemidir. Mikroservis A’nın, mikroservis B’ye bir istek gönderdiğini varsayalım, mikroservis B’nin yanıt vermesi için bir zaman sınırı belirler. Mikroservis B, bu süre içinde yanıt vermezse, bir arızası olduğu kabul edilecektir. Time Limiter ile bu özellik sağlanabilmektedir.

Retry Nedir?

Bir mikroservis A’nın, başka bir mikroservis olan B’ye bağlı olduğunu varsayalım. Diyelim ki mikroservis B, sorunlu bir servis ve başarı oranı sadece %50–60 kadar. Ancak hata, hizmetin mevcut olmaması, sorunlu servisin bazen yanıt verip bazen yanıt vermemesi veya aralıklı bir ağ arızası gibi nedenlerden kaynaklanabilir. Ancak bu durumda mikroservis A, 2–3 kez istek göndermeyi denerse yanıt alma şansı artar. Bu işlevsellik Retry ile sağlanabilmektedir.

Spring Boot ile Resilience4j:

  • Projenin veya modülün pom.xml dosyasına aşağıdaki bağımlılıkları ekle.
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.7.0</version>
</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>
  • Bulkhead, Rate Limiter, Time Limiter, Retry için application.yml içerisine tanımlar ekle.
resilience4j:
# BULKHEAD
bulkhead:
instances:

# Bulkhead object used in RatingServiceClient.getProductRatingDto()
ratingService:
maxConcurrentCalls: 10
maxWaitDuration: 10ms

# RATE LIMITER
ratelimiter:
instances:

# RateLimiter object used in RateLimitingService.basicExample()
basicExample:
limitForPeriod: 2
limitRefreshPeriod: 1s
timeoutDuration: 1s

# RateLimiter object used in RateLimitingService.timeoutExample()
timeoutExample:
limitForPeriod: 2
limitRefreshPeriod: 1s
timeoutDuration: 250ms

# RateLimiter object used in RateLimitingService.multipleRateLimitsExample()
multipleRateLimiters_rps_limiter:
limitForPeriod: 2
limitRefreshPeriod: 1s
timeoutDuration: 2s

# RateLimiter object used in RateLimitingService.searchFlights()
multipleRateLimiters_rpm_limiter:
limitForPeriod: 40
limitRefreshPeriod: 1m
timeoutDuration: 2s

# RateLimiter object used in RateLimitingService.changeLimitsExample()
changeLimitsExample:
limitForPeriod: 1
limitRefreshPeriod: 1s
timeoutDuration: 1s

# RateLimiter object used in RateLimitingService.retryAndRateLimitExample()
retryAndRateLimitExample:
limitForPeriod: 1
limitRefreshPeriod: 1s
timeoutDuration: 250ms

# RateLimiter object used in RateLimitingService.fallbackExample()
fallbackExample:
limitForPeriod: 1
limitRefreshPeriod: 1s
timeoutDuration: 500ms

# RateLimiter object used in RateLimitingService.rateLimiterEventsExample()
rateLimiterEventsExample:
limitForPeriod: 1
limitRefreshPeriod: 1s
timeoutDuration: 50ms

# TIME LIMITER
timelimiter:
instances:

# TimeLimiter object used in TimeLimitingService.basicExample()
basicExample:
timeoutDuration: 2s

# TimeLimiter object used in TimeLimitingService.timeoutExample()
timeoutExample:
timeoutDuration: 500ms

# TimeLimiter object used in TimeLimitingService.eventsExample()
eventsExample:
timeoutDuration: 2s

# TimeLimiter object used in TimeLimitingService.fallbackExample()
fallbackExample:
timeoutDuration: 500ms

# RETRY
retry:
instances:

# Retry object used in RetryingService.basicExample()
basic:
maxRetryAttempts: 3
waitDuration: 2s

# Retry object used in RetryingService.basicExample_serviceThrowingException()
throwingException:
maxRetryAttempts: 3
# ignoreExceptions
retryExceptions:
- java.lang.Exception
waitDuration: 10s

# Retry object used in RetryingService.predicateExample()
predicateExample:
maxRetryAttempts: 3
resultPredicate: io.reflectoring.resilience4j.springboot.predicates.ConditionalRetryPredicate
waitDuration: 3s

# Retry object used in RetryingService.intervalFunctionRandomExample()
intervalFunctionRandomExample:
enableRandomizedWait: true
maxRetryAttempts: 3
randomizedWaitFactor: 0.5
waitDuration: 2s

# Retry object used in RetryingService.intervalFunctionExponentialExample()
intervalFunctionExponentialExample:
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
maxRetryAttempts: 6
waitDuration: 1s

# Retry object used in RetryingService.loggedRetryExample()
loggedRetryExample:
maxRetryAttempts: 3
waitDuration: 2s

# Retry object used in RetryingService.fallbackExample()
fallbackExample:
maxRetryAttempts: 3
waitDuration: 2s

Yukarıdaki tanımlara ve detaylarına resilience4j docs linkinden erişebilirsiniz.

  • Ardından, external servisleri çağıran methodlara ilgili annotasyon’ları ekle.
@Bulkhead(name = "ratingService", fallbackMethod = "getDefault")
public ProductRatingDto getProductRatingDto(int productId){
return this.restTemplate.getForEntity(this.ratingService + productId, ProductRatingDto.class)
.getBody();
}

public ProductRatingDto getDefault(int productId, Throwable throwable){
return ProductRatingDto.of(0, Collections.emptyList());
}
@RateLimiter(name = "basicExample")
List<Flight> basicExample(SearchRequest request) {
return remoteSearchService.searchFlights(request);
}

@RateLimiter(name = "timeoutExample")
List<Flight> timeoutExample(SearchRequest request) {
return remoteSearchService.searchFlights(request);
}

@RateLimiter(name = "multipleRateLimiters_rps_limiter")
List<Flight> multipleRateLimitsExample(SearchRequest request) {
return rpmRateLimitedFlightSearchSearch.searchFlights(request, remoteSearchService);
}

@RateLimiter(name = "multipleRateLimiters_rpm_limiter")
List<Flight> searchFlights(SearchRequest request, FlightSearchService remoteSearchService) {
return remoteSearchService.searchFlights(request);
}

@RateLimiter(name = "changeLimitsExample")
public List<Flight> changeLimitsExample(SearchRequest request) {
return remoteSearchService.searchFlights(request);
}

@Retry(name = "retryAndRateLimitExample")
@RateLimiter(name = "retryAndRateLimitExample")
public List<Flight> retryAndRateLimit(SearchRequest request) {
return remoteSearchService.searchFlights(request);
}

@RateLimiter(name = "rateLimiterEventsExample")
public List<Flight> rateLimiterEventsExample(SearchRequest request) {
return remoteSearchService.searchFlights(request);
}

public void updateRateLimits(String rateLimiterName, int newLimitForPeriod, Duration newTimeoutDuration) {
io.github.resilience4j.ratelimiter.RateLimiter limiter = registry.rateLimiter(rateLimiterName);
limiter.changeLimitForPeriod(newLimitForPeriod);
limiter.changeTimeoutDuration(newTimeoutDuration);
}

@RateLimiter(name = "fallbackExample", fallbackMethod = "localCacheFlightSearch")
public List<Flight> fallbackExample(SearchRequest request) {
return remoteSearchService.searchFlights(request);
}

private List<Flight> localCacheFlightSearch(SearchRequest request, RequestNotPermitted rnp) {
System.out.println("Returning search results from cache");
return Arrays.asList(
new Flight("XY 765", request.getFlightDate(), request.getFrom(), request.getTo()),
new Flight("XY 781", request.getFlightDate(), request.getFrom(), request.getTo()));
}
@TimeLimiter(name = "basicExample")
CompletableFuture<List<Flight>> basicExample(SearchRequest request) {
return CompletableFuture.supplyAsync(() -> remoteSearchService.searchFlightsTakingOneSecond(request));
}

@TimeLimiter(name = "timeoutExample")
CompletableFuture<List<Flight>> timeoutExample(SearchRequest request) {
return CompletableFuture.supplyAsync(() -> remoteSearchService.searchFlightsTakingOneSecond(request));
}

@TimeLimiter(name = "eventsExample")
CompletableFuture<List<Flight>> eventsExample(SearchRequest request) {
return CompletableFuture.supplyAsync(() -> remoteSearchService.searchFlightsTakingRandomTime(request));
}

@TimeLimiter(name = "fallbackExample", fallbackMethod = "localCacheFlightSearch")
CompletableFuture<List<Flight>> fallbackExample(SearchRequest request) {
return CompletableFuture.supplyAsync(() -> remoteSearchService.searchFlightsTakingOneSecond(request));
}

private CompletableFuture<List<Flight>> localCacheFlightSearch(SearchRequest request, TimeoutException rnp) {
System.out.println("Returning search results from cache");
System.out.println(rnp.getMessage());
CompletableFuture<List<Flight>> result = new CompletableFuture<>();
result.complete(Arrays.asList(
new Flight("XY 765", request.getFlightDate(), request.getFrom(), request.getTo()),
new Flight("XY 781", request.getFlightDate(), request.getFrom(), request.getTo())));
return result;
}
@Retry(name = "basic")
public List<Flight> basicExample(SearchRequest request) {
return remoteSearchService.searchFlights(request);
}

@Retry(name = "throwingException")
public List<Flight> searchFlightsThrowingException(SearchRequest request) throws Exception {
return remoteSearchService.searchFlightsThrowingException(request);
}

@Retry(name = "predicateExample")
SearchResponse predicateExample(SearchRequest request) throws IOException {
return remoteSearchService.httpSearchFlights(request);
}

@Retry(name = "intervalFunctionRandomExample")
public List<Flight> intervalFunctionRandom(SearchRequest request) {
return remoteSearchService.searchFlights(request);
}

@Retry(name = "intervalFunctionExponentialExample")
public List<Flight> intervalFunctionExponential(SearchRequest request) {
return remoteSearchService.searchFlights(request);
}

@Retry(name = "loggedRetryExample")
public List<Flight> loggedRetryExample(SearchRequest request) {
return remoteSearchService.searchFlights(request);
}

@Retry(name = "fallbackExample", fallbackMethod = "localCacheFlightSearch")
public List<Flight> fallbackExample(SearchRequest request) {
return remoteSearchService.searchFlights(request);
}

private List<Flight> localCacheFlightSearch(SearchRequest request, RuntimeException re) {
System.out.println("Returning search results from cache");
return Arrays.asList(
new Flight("XY 765", request.getFlightDate(), request.getFrom(), request.getTo()),
new Flight("XY 781", request.getFlightDate(), request.getFrom(), request.getTo()));
}

En temelde, Bulkhead, Rate Limiter, Time Limiter, Retry’yi configure etmek için ihtiyacımız olan şeyler bunlar.

Akbank Teknoloji çatısı altında çalışmalarına devam ettiğimiz konuların devamı için bizleri takip edin.

Bu makaleyi hazırlarken bana yardımcı olan takım arkadaşım Ziya Orujaliyev’e katkılarından dolayı teşekkür ediyorum.

Referanslar:
https://resilience4j.readme.io
https://www.baeldung.com/spring-boot-resilience4j

--

--