라이브러리

resilience4j springboot2 설명 및 구현

탄생 2022. 9. 14. 17:09

● resilience4j 소개

  • Resilience4j 는 사용하기 쉽고 가볍게 만들어진 장애허용(fault tolerance) 라이브러리이다.
  • Netflix Hystrix 영감을 받았으며 java8과 함수형 프로그램밍을 위해 설계되었다.
  • 다른 외부 라이브러리의 종속성이 없는 Vavr(https://www.vari.io)만 사용하여 가볍다.
  • 코어모듈
코어모듈 간략 설명
circuitbreaker 호출결과에 따른 상태 관리
ratelimiter 호출횟수 제한
bulkhead 병렬(스레드) 제한
timelimiter 실행시간 제한
retry 호출 재시도
cache 캐시 구현제 정의

코어모듈 소개

1. circuitbreaker (호출결과에 따른 상태 관리)

  • 호출의 결과가 성공이 아닐 경우(오랜 시간 결과가 없거나 exception 발생 등) 카운팅 되면 일정 횟수가 도달되면 circuitbreaker발생된다.
  •  circuitbreaker는 open, close, half_open의 상태가 있다.  
  • 실패 비율이 설정한 임계치보다 크거나 같을 경우 circuitbreaker의 상태가 close 에서 open으로 변경된다.  
    예를 들어 circuitbreaker의 임계치가 10초에 4번이라고 한다면 API호출시 10초동안 4번의 실패가 발생하면 API를 직접 호출하지 않고 circuitbreaker에 의해 오류메시지를 출력한다.
  • 시간이 지나 circuitbreaker가 반열림 상태가 되어 새로운 호출이 허용되며 문제가 해결되었는지 확인 후 close 상태가 된다.

- circuitbreaker 집계 방법

  • 카운트기반 슬라이등 윈도우(Count-based sliding window)  
    마지막으로 호출한 N번의 결과를 집계하여 circuitbreaker의 상태를 변경한다.
  • 시간 기반 슬라이딩 윈도우(Time-based sliding window)  
    마지막 N초동안 호출결과를 집계하여 circuitbreaker의 상태를 변경한다.

- Circuitbreaker 옵션 정보

속성 설명
failureRateThreshold 실패 비율 임계치를 백분율로 설정한다. 실패 비율이 임계치보다 크거나 같으면 CircuitBreaker는 open 상태로 전환되며, 이때부터 호출을 끊어낸다.
slowCallRateThreshold 임계 값을 백분율로 설정한다. CircuitBreaker는 호출에 걸리는 시간이 slowCallDurationThreshold 보다 길면 느린 호출로 간주한다. 느린 호출 비율이 이 임계치보다 크거나 같으면 CircuitBreaker는 open 상태로 전환되며, 이때부터 호출을 끊어낸다.
slowCallDurationThreshold 호출에 소요되는 시간이 설정한 임계치보다 길면 느린 호출로 계산한다.
permittedNumberOfCallsInHalfOpenState CircuitBreaker가 half open 상태일 때 허용할 호출 횟수를 설정한다.
maxWaitDurationInHalfOpenState CircuitBreaker를 Half Open 상태로 유지할 수 있는 최대 시간으로, 이 시간만큼 경과하면 open 상태로 전환한다. 0일 땐 허용 횟수만큼 호출을 모두 완료할 때까지 HalfOpen 상태로 무한정 기다린다.
slidingWindowType CircuitBreaker가 닫힌 상태에서 호출 결과를 기록할 때 쓸 슬라이딩 윈도우 타입을 설정한다. 슬라이딩 윈도우는 카운트 기반과 시간 기반이 있다. 슬라이딩 윈도우가 COUNT_BASED일 땐 마지막 slidingWindowSize  횟수만큼의 호출을 기록하고 집계한다. TIME_BASED일 땐 마지막 slidingWindowSize 초 동안의 호출을 기록하고 집계한다.
slidingWindowSize CircuitBreaker가 닫힌 상태에서 호출 결과를 기록할 때 쓸 슬라이딩 윈도우의 크기를 설정한다.
minimumNumberOfCalls CircuitBreaker가 실패 비율이나 느린 호출 비율을 계산할 때 필요한 (슬라이딩 윈도우 주기마다) 최소 호출 수를 설정한다. 예를 들어서 minimumNumberOfCalls가 10이라면 최소한 호출을 10번을 기록해야 실패 비율을 계산할 수 있다. 기록한 호출 횟수가 9번 뿐이라면 9번 모두 실패했더라도 CircuitBreaker는 열리지 않는다.
waitDurationInOpenState CircuitBreaker가 open에서 half-open으로 전환하기 전 기다리는 시간.
automaticTransitionFromOpenToHalfOpenEnabled  true로 설정하면 CircuitBreaker는 open 상태에서 자동으로 half-open 상태로 전환하며, 이땐 호출이 없어도 전환을 트리거한다. 
시간이 waitDurationInOpenState  만큼 경과하면 모든 CircuitBreaker 인스턴스를 모니터링해서 HALF_OPEN으로 전환시키는 스레드가 생성된다. 반대로 false로 설정하면 waitDurationInOpenState  만큼 경과하더라도 호출이 한 번은 일어나야 HALF_OPEN으로 전환한다. 이때 좋은 점은 모든 CircuitBreaker의 상태를 모니터링하는 스레드가 없다는 거다.
recordExceptions 실패로 기록해 실패 비율 계산에 포함시킬 예외 리스트.
ignoreExceptions 무시만 하고 실패나 성공으로 계산하지 않는 예외 리스트.
recordFailurePredicate 예외를 실패로 기록할지를 평가하는 커스텀 Predicate. 예외를 실패로 계산해야 할 땐 true를 리턴해야 한다.
ignoreExceptionPredicate 예외를 무시해서 실패나 성공으로 간주하지 않을지를 평가하는 커스텀 Predicate. 예외를 무시해야 할 땐 true를, 실패로 간주해야 할 땐 false를 리턴해야 한다.

2. ratelimiter (호출횟수 제한)

  • 일정시간동안 호출할 수 있는 횟수를 제한한다.
    예를 들어 1초동안 1000번의 호출만 허용한다.
    limitForPeriod / limitRefreshPeriod = 1000 / 1s 로 설정하면 된다.
  • 다량의 서버 요청이 들어왔을 경우 서버허용범위의 호출에 대해서는 결과를 보장해주도록 한다.
  • 제한치를 넘었을 경우 거부를 하게 되며 큐를 만들어 순차 실행할수도 있고 두 정책을 조합할수도 있다.  
    (ratelimiter 의 큐설정에 대해 찾아보았으나 공식문서에도 명확한 방법이 나와있지 않아 라이브러리를 보아야 할것 같다)

- Ratelimiter 옵션 정보

속성 설명
timeoutDuration 스레드가 권한 획득을 기다리는 디폴트 시간
limitRefreshPeriod 제한 횟수의 갱신주기
limitForPeriod 제한 갱신 후 재갱신까지 사용할 호출 횟수

3. bulkhead (병렬 제한)

  • resilience4j에서 동시에 호출할 수 있는 수를 제한합니다.

- Bulkhead 패턴

  • SemaphoreBulkhead
    세마포어 : 공유된 자원의 데이터 혹은 임계영역(Critical Section) 등에 여러 Process 혹은 Thread가 접근하는 것을 막아줌(여러개의 세마포어를 설정하여 해당 수만큼 동시 호출 제한)
  • FixedThreadPoolBulkhead
    유한한 큐와 고정 스레드 풀을 사용

- Bulkhead 옵션 정보

속성 설명
maxConcurrentCalls bulkhead에서 최대로 허용할 병렬 실행 수
maxWaitDuration bulkhead가 포화 상태일 때 진입하려는 스레드를 블로킹할 최대 시간

- ThreadPoolBulkhead 옵션 정보

속성 설명
maxThreadPoolSize 스레드 풀의 최대 사이즈를 설정한다.
coreThreadPoolSize 스레드 풀의 코어 사이즈를 설정한다.
queueCapacity 큐 용량을 설정한다.
keepAliveDuration 스레드 수가 코어보다 많을 때 초과분 만큼의 스레드가 유휴 상태로 새 태스크를 기다리는 최대 시간으로, 이 시간이 지나면 유휴 스레드는 종료된다.

4. timelimiter (실행시간 제한)

  • 호출 시 최대 대기시간, 타임아웃을 설정합니다.
  • timelimiter는 Future형태의 리턴타입을 가져야 합니다.
    Future는 비동기적인 연산의 결과를 표현하는 클래스입니다  
    Future 인터페이스는 java5부터 java.util.concurrency 패키지에서 비동기의 결과값을 받는 용도로 사용했지만 비동기의 결과값을 조합하거나, error를 핸들링할 수가 없었다.
    자바8부터 CompletableFuture 인터페이스가 소개되었고, Future 인터페이스를 구현함과 동시에 CompletionStage 인터페이스를 구현한다. CompletionStage는 비동기 연산 Step을 제공해서 계속 체이닝 형태로 조합이 가능하다.

- TimeLimiter 옵션 정보

속성 설명
timeoutDuration 타임아웃 시간
cancelRunningFuture timeout이 경과한 후 자동으로 future를 취소여부

5. retry (재시도)

  • 실패한 호출에 대하여 재시도를 한다.

- Retry 옵션 정보

속성 설명
maxAttempts 최대로 시도해볼 횟수 (최초 호출도 포함)
waitDuration 재시도할 때마다 기다리는 고정 시간
intervalFunction 실패했을 때 대기할 시간을 수정하는 함수. 기본적으로는 대기 시간을 일정하게 유지한다.
intervalBiFunction 실패했을 때 대기할 시간을 시도 횟수와 결과/예외에 따라 수정하는 함수. intervalFunction과 함께 사용하면 IllegalStateException이 발생한다.
retryOnResultPredicate 반환되는 결과에 따라서 retry를 할지 말지 결정하는 filter, true로 반환하면 retry하고 false로 반환하면 retry 하지 않습니다.
retryExceptionPredicate exception에 따라서 retry 할지 말지 결정하는 filter, true로 반환하면 retry하고 false로 반환하면 retry 하지 않습니다.
retryExceptions 실패로 기록해서 재시도할 Throwable 클래스 목록
ignoreExceptions 무시하고 재시도하지 않을 Throwable 클래스 목록
failAfterMaxAttempts 설정한 maxAttempts 만큼 재시도하고 나서도 결과가 실패라면 MaxRetriesExceededException 발생 활성여부

● resilience4j 설정하기

1. 의존성 추가하기

  • resilience4j-spring-boot2는 spring-boot-starter-actuator와 spring-boot-starter-aop 는 런타임시 필요하다.
  • 만약 springboot2의 webflux를 활용할 경우 io.github.resilience4j:resilience4j-reactor 도 필요하다.
implementation 'io.github.resilience4j:resilience4j-spring-boot2:1.7.1'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-aop'


2. 스프링 부트 설정(application.yml)

  • 위의 각 해당 모듈의 옵션 정보를 활용하여 정보를 입력합니다.
  • 사용할 모듈정보만 입력하여 사용하면 됩니다.
    호출제한만 사용하겠다고 한다면 ratelimiter 만 입력하며 사용
  • 만약 동일 모듈에 대해 API별로 제한 정보가 다르다면 instances를 활용하여 여러개의 설정을 할 수 있습니다.
resilience4j:
  circuitbreaker:
    configs:
      default:
        registerHealthIndicator: true
        slidingWindowSize: 10
        permittedNumberOfCallsInHalfOpenState: 3
        automaticTransitionFromOpenToHalfOpenEnabled: true
        waitDurationInOpenState: 5s
        failureRateThreshold: 50
        eventConsumerBufferSize: 10
  ratelimiter:
    configs:
      default:
        limitForPeriod: 10
        limitRefreshPeriod: 10s
        timeoutDuration: 3s
    instances:
      backendA:
        base-config: default
        limitForPeriod: 5
        limitRefreshPeriod: 10s
      backendB:
        limitForPeriod: 2
        limitRefreshPeriod: 10s
        timeoutDuration: 0s
  bulkhead:
    configs:
      default:
        maxConcurrentCalls: 1
        maxWaitDuration: 0
  thread-pool-bulkhead:
    configs:
      default:
        maxThreadPoolSize: 1
        coreThreadPoolSize: 1
        queueCapacity: 1
  timelimiter:
    configs:
      default:
        cancelRunningFuture: true
        timeoutDuration: 2s
  retry:
    configs:
      default:
        maxAttempts: 3
        waitDuration: 10s
        enableExponentialBackoff: true
        exponentialBackoffMultiplier: 2
        retryExceptions:
          - org.springframework.web.client.HttpServerErrorException
          - java.io.IOException
        ignoreExceptions:
          - java.lang.RuntimeException


3. resilience4j 적용하기

  • resilience4j 적용할 api에 해당 annotation을 추가하면 동작됩니다.
  • 여러 모듈을 사용할 경우 하나의 API에 사용할 모듈 annotation을 모두 적용하면 됩니다.
  • 설정의 instances 정보는 annotation 의 name 필드와 매핑되며 매핑되지 않는 name은 default 값으로 정의됩니다.
  • fallbackMethod 를 통해 한계치에 도달한 처리를 custom하게 보여줄수 있습니다.
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
public class Resilience4jController {

    @GetMapping("/bulkhead")
    @Bulkhead(name = "bulkhead", type = Bulkhead.Type.SEMAPHORE)
    public String getBulkhead(){
        return "OK";
    }

    @GetMapping("/circuit")
    @CircuitBreaker(name = "circuitBreaker")
    public String getCircuitBreaker(boolean isError){
        if(isError){
            throw new RuntimeException("예기치 않은 에러 발생");
        }
        return "OK";
    }

    @GetMapping("/rate-limiter-default")
    @RateLimiter(name = "rateLimiter")
    public String getRateLimiterDefault(){
        return "OK";
    }

    @GetMapping("/rate-limiter-instance")
    @RateLimiter(name = "backendB", fallbackMethod = "fallback")
    public String getRateLimiterInstance(){
        return "OK";
    }

    @GetMapping("/time-limiter")
    @TimeLimiter(name = "timeLimiter")
    public CompletableFuture<String> getPingPongTimeLimiter() {
        return CompletableFuture.supplyAsync(() -> {
            try {
                sleep(10000);
            } catch (InterruptedException e) {
                log.error("error", e);
                Thread.currentThread().interrupt();
            }
            return "OK";
        });
    }

    @GetMapping("/retry")
    @CircuitBreaker(name = "retry")
    public String getRetry(boolean isError){
        if(isError){
            throw new RuntimeException("예기치 않은 에러 발생");
        }
        return "OK";
    }

    private static String fallback(Exception exception) {
        return "error: " + exception.getMessage();
    }
}

● git 주소

https://bitbucket.org/technical-space/resilience4j-spring-boot2/src/master/


참고 URL