Spring Cloud

Resilience4j, Spring Boot 설정 공부

Core modules

Resilience4j는 5개의 Core modules을 제공합니다.

CircuitBreaker

CircuitBreaker 패턴을 구현한 모듈입니다. 호출의 결과를 저장하고 집계(Aggregate)하기 위한 슬라이딩 윈도를 사용합니다.

 

요청 실패율(Failure rate)이 몇 퍼센트 이상일 때 서킷의 상태가 CLOSED에서 OPEN으로 바뀔지 그리고 요청 중 지연된 응답(Slow calls)이 몇 퍼센트 이상일 때 서킷의 상태가 CLOSED에서 OPEN으로 바뀔지 설정할 수 있습니다.

 

슬라이딩 윈도우 크기가 15인 경우에도. 슬라이딩 윈도우가 15개의 요청만 동시에 실행할 수 있다는 것을 의미하지는 않습니다. 동시 스레드 수를 제한하려면 벌크헤드를 사용해야한다.(아래에서 다룸)

 

두 가지 종류의 윈도우 중 하나를 선택하여 사용할 수 있습니다.

1. 마지막 N개의 요청의 결과를 집계하는 카운트 베이스 윈도우(count-based window)

 - 설정한 window size N만큼의 값을 가질 수 있는 circular array로 구현

 - 새 요청 결과가 기록되면 업데이트되며 가장 오래된 측정값이 제거되면 측정값이 총집계에서 차감되고 버킷 재설정

 - 스냅샷은 미리 집계되고 창 크기와 독립적이므로 스냅샷 검색 시간 복잡도는 O(1)

 - 공간 요구 사항(메모리 소비량)은 O(n)

 

2. 마지막 N초 동안 요청의 결과를 집계하는 시간 베이스 윈도우(time-based window)

 - N 부분 집합(버킷)의  circular array로 구현

 - 모든 버킷은 특정 초(epoch second)에 발생하는 모든 요청의 결과를 집계

 - 가장 오래된 버킷이 제거되면 해당 버킷의 총 집계가 총 집계에서 부분적으로 차감되고 버킷 재설정

 - 스냅샷은 미리 집계되고 기간 크기와 독립적이므로 스냅샷 검색 시간 복잡도는 O(1)

 - 공간 요구 사항(메모리 소비)은 거의 일정한 O(n), N개의 부분집계와 1개의 총집계만 생성

 

Config propertyDefault ValueDescription
failureRateThreshold 50 실패율(failure ratio) threshold퍼센트 값

실패율 임계값보다 크거나 같으면 CircuitBreaker가 개방으로 전환하고 단락 호출을 시작
slowCallRateThreshold 100 지연된 응답(failure ratio) threshold 퍼센트 값

느린 호출의 백분율이 임계값과 같거나 크면 CircuitBreaker가 개방으로 전환되고 단락 호출이 시작
slowCallDurationThreshold 60000 [ms] 요청이 느린 것으로 간주되는 기간
permittedNumberOfCalls
InHalfOpenState
10 Half-open 상태에서 허가된 요청 수
maxWaitDurationInHalfOpenState 0 Half-open 상태에서 대기할 수 있는 최대 시간

0은 무한정 기다리는 것을 의미
slidingWindowType COUNT_BASED  CircuitBreaker가 닫혔을 때 호출 결과를 기록하는 데 사용되는 슬라이딩 윈도우의 유형을 구성합니다.

COUNT_BASED이면 마지막 슬라이딩 WindowSize 호출이 기록되고 집계

TIME_BASSED이면 마지막 슬라이딩 WindowSize 초의 호출이 기록되고 집계
slidingWindowSize 100 서킷의 상태가 CLOSED일 때 요청의 결과를 기록하기 위한 슬라이딩 윈도의 크기
minimumNumberOfCalls 100 서킷이 실패율(failure rate) 또는 지연된 응답(slow call rate)을 계산하기 전 요구되는 최소 요청의 수
waitDurationInOpenState 60000 [ms] 서킷이 OPEN 에서 Half-open으로 변경되기 전 대기하는 시간 (이 시간 이후 변경된다.)
automaticTransition
FromOpenToHalfOpenEnabled
false true 라면 waitDurationInOpenState 기간이 지난 이후에 Open에서 Half-open으로 자동으로 상태가 변경된다. 하나의 쓰레드가 CircuitBreaker의 모든 인스턴스들을 모니터링하며 상태를 확인한다.

false라면 요청이 있을 때만 상태가 Half-open으로 변경된다. 즉, waitDurationInOpenState 기간이 지난 후에 새로운 요청이 있으면 Half-open 상태로 변경된다.모니터링을 위한 쓰레드가 필요없는 이점이 있다.
recordExceptions empty 실패로 기록될 예외의 리스트로 실패율(failure rate)가 증가되는 예외 리스트이다.
예외 리스트를 설정한다면, 다른 모든 예외는 ignoreExceptions에 의해 무시되지 않는다면 성공으로 간주된다.
ignoreExceptions empty 성공 또는 실패로 기록되지 않는 예외들의 리스트이다.
recordException throwable -> true

By default all exceptions are recored as failures.
커스텀 Predicate로 예외가 실패로 기록될지 정의 할 수 있다.
예외가 실패로 카운트 되야 한다면 true를 리턴하고 성공으로 카운트 되야 한다면 false를 리턴해야 한다. (ignoreExceptions에 의해 무시되지 않는 경우)
ignoreException throwable -> false

By default no exception is ignored.
커스텀 Predicate로 예외가 무시될지 정의할 수 있다.
예외가 무시되려면 true를 리턴하고 실패로 카운트 되려면 false를 리턴해야 한다.
// 코드 예시
// Create a custom configuration for a CircuitBreaker
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
  .failureRateThreshold(50)
  .slowCallRateThreshold(50)
  .waitDurationInOpenState(Duration.ofMillis(1000))
  .slowCallDurationThreshold(Duration.ofSeconds(2))
  .permittedNumberOfCallsInHalfOpenState(3)
  .minimumNumberOfCalls(10)
  .slidingWindowType(SlidingWindowType.TIME_BASED)
  .slidingWindowSize(5)
  .recordException(e -> INTERNAL_SERVER_ERROR
                 .equals(getResponse().getStatus()))
  .recordExceptions(IOException.class, TimeoutException.class)
  .ignoreExceptions(BusinessException.class, OtherBusinessException.class)
  .build();

// Create a CircuitBreakerRegistry with a custom global configuration
CircuitBreakerRegistry circuitBreakerRegistry = 
  CircuitBreakerRegistry.of(circuitBreakerConfig);

// Get or create a CircuitBreaker from the CircuitBreakerRegistry 
// with the global default configuration
CircuitBreaker circuitBreakerWithDefaultConfig = 
  circuitBreakerRegistry.circuitBreaker("name1");

// Get or create a CircuitBreaker from the CircuitBreakerRegistry 
// with a custom configuration
CircuitBreaker circuitBreakerWithCustomConfig = circuitBreakerRegistry
  .circuitBreaker("name2", circuitBreakerConfig);
# 스프링부트 설정 예시
resilience4j.circuitbreaker:
    configs:
        default:
            registerHealthIndicator: true
            slidingWindowSize: 10
            minimumNumberOfCalls: 5
            permittedNumberOfCallsInHalfOpenState: 3
            automaticTransitionFromOpenToHalfOpenEnabled: true
            waitDurationInOpenState: 5s
            failureRateThreshold: 50
            eventConsumerBufferSize: 10
            recordExceptions:
                - org.springframework.web.client.HttpServerErrorException
                - java.util.concurrent.TimeoutException
                - java.io.IOException
            ignoreExceptions:
                - io.github.robwin.exception.BusinessException
        shared:
            slidingWindowSize: 100
            permittedNumberOfCallsInHalfOpenState: 30
            waitDurationInOpenState: 1s
            failureRateThreshold: 50
            eventConsumerBufferSize: 10
            ignoreExceptions:
                - io.github.robwin.exception.BusinessException
    instances:
        backendA:
            baseConfig: default

 

Bulkhead

Resilience4j는 동시 실행(Concurrent execution) 수를 제한하는데 사용되는 두개의 bulkhead 패턴 구현체를 제공합니다.

SemaphoreBulkhead는 다양한 스레딩 및 I/O 모델에서 잘 작동

 

벌크헤드 구성과 일치하는 올바른 스레드 풀 크기를 확인하는 것은 클라이언트에 달려 있습니다.

  • Semaphores를 사용하는 SemaphoreBulkhead
  • 고정된 Thread pool과 Bounded queue를 사용하는 FixedThreadPoolBulkHead

 

 

SemaphoreBulkhead Config

Config propertyDefault valueDescription
maxConcurrentCalls 25 Bulkhead에 의해 허가된 동시 실행 최대 수
maxWaitDuration 0 포화 상태의 Bulkhead에 진입하기 위해 block 되는 최대 시간
값이 0인 경우에는 바로 요청을 막는다.
// 코드 예시
// Create a custom configuration for a Bulkhead
BulkheadConfig config = BulkheadConfig.custom()
    .maxConcurrentCalls(150)
    .maxWaitDuration(Duration.ofMillis(500))
    .build();

// Create a BulkheadRegistry with a custom global configuration
BulkheadRegistry registry = BulkheadRegistry.of(config);

// Get or create a Bulkhead from the registry - 
// bulkhead will be backed by the default config
Bulkhead bulkheadWithDefaultConfig = registry.bulkhead("name1");

// Get or create a Bulkhead from the registry, 
// use a custom configuration when creating the bulkhead
Bulkhead bulkheadWithCustomConfig = registry.bulkhead("name2", custom);
# 스프링부트 설정 예시
resilience4j.bulkhead:
    configs:
        default:
            maxConcurrentCalls: 100
    instances:
        backendA:
            maxConcurrentCalls: 10
        backendB:
            maxWaitDuration: 10ms
            maxConcurrentCalls: 20

FixedThreadPoolBulkHead Config

Config propertyDefault valueDescription
maxThreadPoolSize Runtime.getRuntime()
.availableProcessors()
최대 쓰레드 풀 크기
coreThreadPoolSize Runtime.getRuntime()
.availableProcessors() - 1
코어 쓰레드 풀 크기
queueCapacity 100 큐의 크기
keepAliveDuration 20 [ms] 스레드 수가 코어보다 크면 초과된 유휴 스레드가 종료하기 전에 새 작업을 대기하는 최대 시간
// 코드 예시
ThreadPoolBulkheadConfig config = ThreadPoolBulkheadConfig.custom()
  .maxThreadPoolSize(10)
  .coreThreadPoolSize(2)
  .queueCapacity(20)
  .build();
        
// Create a BulkheadRegistry with a custom global configuration
ThreadPoolBulkheadRegistry registry = ThreadPoolBulkheadRegistry.of(config);

// Get or create a ThreadPoolBulkhead from the registry - 
// bulkhead will be backed by the default config
ThreadPoolBulkhead bulkheadWithDefaultConfig = registry.bulkhead("name1");

// Get or create a Bulkhead from the registry, 
// use a custom configuration when creating the bulkhead
ThreadPoolBulkheadConfig custom = BulkheadConfig.custom()
  .maxThreadPoolSize(5)
  .build();

ThreadPoolBulkhead bulkheadWithCustomConfig = registry.bulkhead("name2", custom);
# 스프링부트 설정 예시
resilience4j.thread-pool-bulkhead:
    configs:
        default:
            maxThreadPoolSize: 4
            coreThreadPoolSize: 2
            queueCapacity: 2
    instances:
        backendA:
            baseConfig: default
        backendB:
            maxThreadPoolSize: 1
            coreThreadPoolSize: 1
            queueCapacity: 1

RateLimiter

일정 시간동안 요청 수를 제한하는데 사용됩니다.

 

속도 제한은 API의 확장에 대비하고 서비스의 높은 가용성과 안정성을 확립하기 위해 반드시 필요한 기술

 

제한 초과 요청을 거부하거나 대기열을 작성하여 나중에 실행하거나 이 두 가지 접근 방식을 조합할 수 있습니다.

Config propertyDefault valueDescription
timeoutDuration 5 [s] 허가(Permission)을 위해 쓰레드가 대기하는 기본 시간
limitRefreshPeriod 500 [ns] Limit refresh 기간으로, 각 기간 이후에 RateLimiter가 일정 시간 동안 허가되는 요청 수를 다시 설정한다.
limitForPeriod 50 한 Limit refresh 기간 동안 허가되는 요청 수
// 코드 예시
RateLimiterConfig config = RateLimiterConfig.custom()
  .limitRefreshPeriod(Duration.ofMillis(1))
  .limitForPeriod(10)
  .timeoutDuration(Duration.ofMillis(25))
  .build();

// Create registry
RateLimiterRegistry rateLimiterRegistry = RateLimiterRegistry.of(config);

// Use registry
RateLimiter rateLimiterWithDefaultConfig = rateLimiterRegistry
  .rateLimiter("name1");

RateLimiter rateLimiterWithCustomConfig = rateLimiterRegistry
  .rateLimiter("name2", config);
# 스프링부트 설정 예시
resilience4j.ratelimiter:
    configs:
        default:
            registerHealthIndicator: false
            limitForPeriod: 10
            limitRefreshPeriod: 1s
            timeoutDuration: 0
            eventConsumerBufferSize: 100
    instances:
        backendA:
            baseConfig: default
        backendB:
            limitForPeriod: 6
            limitRefreshPeriod: 500ms
            timeoutDuration: 3s

Retry

요청이 실패했을 경우 재시도 정책에 관련한 조건을 관리하기 위해 사용됩니다.

Config propertyDefault valueDescription
maxAttempts 3 최대 재시도 수
waitDuration 500 [ms] 재시도 사이에 고정된 대기 시간
intervalFunction numOfAttempts -> waitDuration 요청 실패 이후 대기 간격을 수정하기 위한 함수이다. 기본적으로는 대기 시간(waitDuration)이 일정하게 유지된다.
retryOnResultPredicate result -> false 결과에 따라 재시도 여부를 결정하기 위한 Predicate를 설정한다.
만약 결과가 재시도 되야 한다면, true를 리턴해야하고 그렇지 않다면 false를 리턴해야 한다.
retryOnExceptionPredicate throwable -> true 예외(Exception)에 따라 재시도 여부를를 결정하기 위한 Predicate를 설정한다.
만약 예외에 따라 재시도 되야 한다면, true를 리턴해야하고 그렇지 않다면 false를 리턴해야 한다.
retryExceptions empty 실패로 기록되는 에러 클래스 리스트 즉, 재시도 되야하는 에러 클래스의 리스트이다.

empty일 경우 모든 에러 클래스를 재시도 한다.
ignoreExceptions empty 무시되야 하는 에러 클래스 리스트 즉, 재시도 되지 않아야 할 에러 클래스 리스트이다.
// 코드 예시
RetryConfig config = RetryConfig.custom()
  .maxAttempts(2)
  .waitDuration(Duration.ofMillis(1000))
  .retryOnResult(response -> response.getStatus() == 500)
  .retryOnException(e -> e instanceof WebServiceException)
  .retryExceptions(IOException.class, TimeoutException.class)
  .ignoreExceptions(BusinessException.class, OtherBusinessException.class)
  .failAfterMaxAttempts(true)
  .build();

// Create a RetryRegistry with a custom global configuration
RetryRegistry registry = RetryRegistry.of(config);

// Get or create a Retry from the registry - 
// Retry will be backed by the default config
Retry retryWithDefaultConfig = registry.retry("name1");

// Get or create a Retry from the registry, 
// use a custom configuration when creating the retry
RetryConfig custom = RetryConfig.custom()
    .waitDuration(Duration.ofMillis(100))
    .build();

Retry retryWithCustomConfig = registry.retry("name2", custom);
# 스프링부트 설정 예시
resilience4j.retry:
    configs:
        default:
            maxAttempts: 3
            waitDuration: 100
            retryExceptions:
                - org.springframework.web.client.HttpServerErrorException
                - java.util.concurrent.TimeoutException
                - java.io.IOException
            ignoreExceptions:
                - io.github.robwin.exception.BusinessException
    instances:
        backendA:
            baseConfig: default
        backendB:
            baseConfig: default

TimeLimiter

원격 서버를 호출하는데 걸리는 시간을 제한할 수 있습니다.

Config propertyDefault valueDescription
timeoutDuration 1 [s] Timeout 값 (기본 단위는 ms)
cancelRunningFuture true Timeout 발생 후 future를 취소할지 결정하는 Boolean 값
// 코드 예시
TimeLimiterConfig config = TimeLimiterConfig.custom()
   .cancelRunningFuture(true)
   .timeoutDuration(Duration.ofMillis(500))
   .build();

// Create a TimeLimiterRegistry with a custom global configuration
TimeLimiterRegistry timeLimiterRegistry = TimeLimiterRegistry.of(config);

// Get or create a TimeLimiter from the registry - 
// TimeLimiter will be backed by the default config
TimeLimiter timeLimiterWithDefaultConfig = registry.timeLimiter("name1");

// Get or create a TimeLimiter from the registry, 
// use a custom configuration when creating the TimeLimiter
TimeLimiterConfig config = TimeLimiterConfig.custom()
   .cancelRunningFuture(false)
   .timeoutDuration(Duration.ofMillis(1000))
   .build();

TimeLimiter timeLimiterWithCustomConfig = registry.timeLimiter("name2", config);
# 스프링부트 설정 예시
resilience4j.timelimiter:
    configs:
        default:
            cancelRunningFuture: false
            timeoutDuration: 2s
    instances:
        backendA:
            baseConfig: default
        backendB:
            baseConfig: default

Reference

https://github.com/resilience4j/resilience4j-spring-boot2-demo

https://resilience4j.readme.io/docs

https://leejongchan.tistory.com/100