본문 바로가기

개발(합니다)/Java&Spring

[spring boot 설정하기-24] spring cloud Resilience4j(1) 설정 및 테스트 소스

반응형

1. 개요

https://resilience4j.readme.io/docs/getting-started

Hystrix에서 영감을 얻어 만든 라이브러리로 Resilience4j입니다.
가볍고 다른 라이브러리에 의존성이 없고
Circuit Breaker, Rate Limier, Time Limiter, Bulkhead, Retry, Cache 구현체가 있습니다.

2. 설정 설명

아래 사이트를 참고합니다.

https://resilience4j.readme.io/docs/circuitbreaker

1. CircuitBreaker Config

요청 실패율이 특정 값 이상일 경우 서킷의 상태를 open/close 여부를 결정하여 에러에 대한 대응을 할 수 있습니다.

Config propertyDefault ValueDescription
failureRateThreshold 50 실패율(failure ratio) threshold % 
slowCallRateThreshold 100 지연된 응답 threshold % 
slowCallDurationThreshold 60000 [ms] 요청이 느린 것으로 간주하는 ms 초
permittedNumberOfCalls
InHalfOpenState
10 Half-open 상태에서 허가되는 요청 수
maxWaitDurationInHalfOpenState 0 Half-open 상태에서 대기할 수 있는 최대 시간으로 모든 허가된 요청이 완료될 때까지 0은 무기한으로 기다림
slidingWindowType COUNT_BASED  COUNT_BASED 또는 TIME_BASED로 호출의 결과를 저장하고 집계하기 위한 슬라이딩 윈도의 타입
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를 리턴

2. Bulkhead

동시에 실행될 수 있는 수를 제한합니다.

SemaphoreBulkhead Config

Config propertyDefault valueDescription
maxConcurrentCalls 25 Bulkhead에 의해 허가된 동시 실행 최대 수
maxWaitDuration 0 포화 상태의 Bulkhead에 진입하기 위해 block 되는 최대 시간
값이 0인 경우에는 바로 요청을 제한

FixedThreadPoolBulkHead Config

Config propertyDefault valueDescription
maxThreadPoolSize Runtime.getRuntime()
.availableProcessors()
최대 쓰레드 풀 크기
coreThreadPoolSize Runtime.getRuntime()
.availableProcessors() - 1
코어 쓰레드 풀 크기
queueCapacity 100 큐의 크기
keepAliveDuration 20 [ms] 스레드 수가 코어보다 크면 초과된 유휴 스레드가 종료하기 전에 새 작업을 대기하는 최대 시간

3.RateLimiter

요청 수를 일정시간 동안 제한합니다.

Config propertyDefault valueDescription
timeoutDuration 5 [s] 허가(Permission)을 위해 쓰레드가 대기하는 기본 시간
limitRefreshPeriod 500 [ns] Limit refresh 기간으로, 각 기간 이후에 RateLimiter가 일정 시간 동안 허가되는 요청 수를 다시 설정
limitForPeriod 50 한 Limit refresh 기간 동안 허가되는 요청 수

4.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 무시되야 하는 에러 클래스 리스트 즉, 재시도 되지 않아야 할 에러 클래스 리스트
failAfterMaxRetries
false MaxRetries의 던지기를 활성화하거나 비활성화하는 부울초과됨재시도가 구성된 최대 시도 수에 도달했지만 결과가 여전히 재시도OnResultPredicate를 통과하지 못하는 경우 예외

5.TimeLimiter

요청 호출에 대한 소요되는 시간을 제한합니다.

Config propertyDefault valueDescription
timeoutDuration 1 [s] Timeout 값 (기본 단위는 ms)
cancelRunningFuture true Timeout 발생 후 future를 취소할지 결정하는 Boolean 값

circuit break 사용 방법만 작성합니다.
cloud-server와 cloud-service 중 coude-service에 해당하는 라이브러리로 cloud-resilience4j 1개로만 진행합니다.

1. 의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-aop'가 추가되어야 circuit breaker를 사용할 수 있습니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'

    implementation 'io.micrometer:micrometer-registry-prometheus'
}

2. application.yml

server:
  port: 7979

resilience4j:
  circuitbreaker:
    configs:
      default:
        registerHealthIndicator: true # actuator를 통해 circuitbraker 상태를 확인하기 위해 설정
        minimumNumberOfCalls: 5 # Circuit Breaker가 에러 비율 또 slow call 비율을 계산하기 전에 요구되는 최소한의 요청 수
        failureRateThreshold: 50  # 에러 비율 (퍼센트)로 해당 값 이상으로 에러 발생시 서킷이 Open 된다.
        waitDurationInOpenState: 10s  # 서킷의 상태가 Open에서 Half-open으로 변경되기 전에 Circuit Breaker가 기다리는 시간

    instances:
      testCircuitBreaker:
        baseConfig: default

  bulkhead:
    instances:
      testCircuitBreaker:
        maxConcurrentCalls: 25  # 허가된 동시 실행 수를 25로 지정
        maxWaitDuration: 0  # 포화 상태의 Bulkhead에 진입하기 위해 block 되는 최대 시간, 값이 0이므로 바로 요청을 막는다.


  ratelimiter:
    instances:
      testCircuitBreaker:
        limitForPeriod: 50  # limitRefreshPeriod 기간 동안 허용되는 요청 수
        limitRefreshPeriod: 500ns # limit refresh 기간
        timeoutDuration: 5s # 허가를 위해 쓰레드가 대기하는 기본 시간
        registerHealthIndicator: true

# fallback method가 정의되어있지 않은 에러의 경우에만 재시도 한다.
  retry:
    instances:
      testCircuitBreaker:
        maxRetryAttempts: 3 # 최대 재시도 수
        waitDuration: 500ms # 재시도 사이에 고정된 시간
#        retryExceptions:
        # Empty 일 경우 모든 에러 클래스에 대해 재시도
  #        - org.springframework.web.client.HttpServerErrorException
  #        - io.github.resilience4j.circuitbreaker.Exception

  timelimiter:
    instances:
      testCircuitBreaker:
        timeoutDuration: 1s # 원격 서버로부터 해당 시간안에 응답이 오는 것을 제한



management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    shutdown:
      enabled: true
    health:
      show-details: always # actuator에서 디테일한 정보들을 확인하기 위해 설정
  health:
    circuitbreakers:
      enabled: true # actuator를 통해 circuitbraker 상태를 확인하기 위해 설정

3. Resilience4jContoller.java

package com.otrodevym.cloudresilience4j;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;


@RestController
@RequiredArgsConstructor
@RequestMapping("/resilience4j")
public class Resilience4jContoller {

    private final TestService testService;


    @GetMapping("/fail")
    public Mono<String> fail() {
        return testService.getFailHello();
    }

    @GetMapping("/success")
    public Mono<String> success() {
        return testService.getSucessHello();
    }

    @GetMapping("/data")
    public Mono<String> data() {
        return testService.getData();
    }


}

4. TestService.java

package com.otrodevym.cloudresilience4j;

import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import io.github.resilience4j.retry.annotation.Retry;
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.util.Collections;


@Service
public class TestService {
    private static final String TEST_CIRCUIT_BREAKER_NAME = "testCircuitBreaker";


    @Bulkhead(name = TEST_CIRCUIT_BREAKER_NAME)
    @CircuitBreaker(name = TEST_CIRCUIT_BREAKER_NAME, fallbackMethod = "fallback")
    public Mono<String> getFailHello() {
        return Mono.error(new CustomException("CustomException"));
    }

    @RateLimiter(name = TEST_CIRCUIT_BREAKER_NAME)
    @Bulkhead(name = TEST_CIRCUIT_BREAKER_NAME)
    @CircuitBreaker(name = TEST_CIRCUIT_BREAKER_NAME, fallbackMethod = "fallback")
    public Mono<String> getSucessHello() {
        return Mono.just("hello");
    }

    @TimeLimiter(name = TEST_CIRCUIT_BREAKER_NAME)
    @Retry(name = TEST_CIRCUIT_BREAKER_NAME)
    @CircuitBreaker(name = TEST_CIRCUIT_BREAKER_NAME, fallbackMethod = "fallback")
    public Mono<String> getData() {
        WebClient webClient = WebClient.builder()
                .baseUrl("http://localhost:8080")
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080"))
                .build();
        webClient.get();

        return webClient.method(HttpMethod.GET)
                .uri("/")
                .retrieve()
                .bodyToMono(String.class);

    }


    public Mono<String> fallback(CustomException e) {
//        e.printStackTrace();
        return Mono.just("fallback");
    }

    public Mono<String> fallback(CallNotPermittedException e) {
//        e.printStackTrace();
        return Mono.just("CallNotPermittedException");
    }
}

5. CustomException.javva

package com.otrodevym.cloudresilience4j;


public class CustomException extends Exception{
    public CustomException(String customException) {
        super(customException);
    }
}

6. 설정 확인 및 테스트

6-1 actuator 확인

http://localhost:7979/actuator 에 접속해서 circuit을 찾으면 actuator에 추가 되어 있습니다.

6-2. actuator/health 확인

http://localhost:7979/actuator/health에 접속하면 circuit breaker의 상태를 확인할 수 있습니다.

 

6-3. /resilience4j/fail로 fallback 확인

 

.http를 이용해 5번에 오류를 발생시켜 close에서 open이 되도록 해보겠습니다.

우선 아래 빨간색으로 표시 된 부분은 4번 요청했고 3번 실패한 현재 상태를 표시하고 있고

5번 요청했을 때 실패율에 따라 close와 open을 결정합니다.

 

 

 

GET localhost:7979/resilience4j/fail

 

왼쪽은 CustomException이 발생하여 fallback으로 처리되는 결과입니다.

오른쪽은 빠르게 여러번 호출하여 제한이 걸린 결과입니다. 조금 후에 다시 시도 하면 다시 fallback으로 떨어집니다.

 

에러를 내고 확인해보면 state가 "OPEN"이 된것을 확인할 수 있습니다.

 

일정 시간이 지나고 나서 다시 에러를 발생시키면 "HALF_OPEN"이 된 것을 확인할 수 있습니다.

 

success로 일정 비율로 성공시키면 다시 close가 된것을 확인할 수 있습니다.

 

 

반응형