본문 바로가기
개발/SpringBoot

SpringBoot3 CircuitBreaker Resilience4j 알아보기

by 궁즉변 변즉통 통즉구 2023. 10. 8.
반응형

CircuitBreaker

Fault Tolerance(=장애 허용 시스템) 에서 사용되는 대표적인 패턴으로 서비스에서 타 서비스 호출 시 에러, 응답지연, 무응답, 일시적인 네트워크 문제 등을 요청이 무작위로 실패하는 경우에 Circuit를 오픈하여 장애가 다른 서비스로 전파되지 못하도록 방지하고 미리 정의해놓은 Fallback Response를 보내어 서비스 장애가 전파되지 않도록 하는 패턴이다. CircuitBreaker를 사용하는 목적은 애플리케이션의 안정성과 장애 저항력을 높이는 데 있다.

  • 상태 정상
    • Client -> Service A -> Circuit Breaker (정상 상태: Bypass Traffic) -> Service B
  • 장애상황
    • Client -> Service A <-> Circuit Breaker (상태: 장애상황이므로 Fallback Message 처리) | Service B (Circuit Breaker 에 의해서 Service B 까지 요청이 도달하지 않음)

서킷브레이커 상태

상태 설명
CLOSED 초기 상태로 모든 접속이 일반적으로 실행
OPEN 에러율이 임계치를 넘어가면 OPEN 상태로 변경, 모든 접속 차단(Fail Fast)
실제 요청을 실행하지 않고 바로 에러 및 디폴트 응답으로 처리
HALF_OPEN OPEN 상태 중간에 한번씩 요청을 날려 응답이 성공인하는지 체크하는 상태
OPEN 후 일정 시간이 지나면 HALF_OPEN 상태로 변경 됨
중간중간 접속 체크에서 성공하면  CLOSE, 실패하면 OPEN으로 변경처리 함

 

Java 기반 CircuitBreaker

1. Netflix Hystrix

  • Netflix OSS 제품군, Spring과 Integration해서 Spring Cloud Netflix 프로젝트를 제공하였음
  • 현재 지원종료 상태(Spring Boot 2.4.X 부터는 더 이상 지원하지 않음), Spring에서는 아래 내용을 권장

 

2. Resilience4j

  • 사용하기 가볍고, 다른 라이브러리 의존성 없음
  • Java 전용으로 개발된 경량화된 Fault Tolerance Libray
  • Spring에서 Hystrix 대안으로 권장하는 라이브러리

 

Resilience4j

resilience4j는 CircuitBreaker 외에 아래와 같이 다양한 기능들을 제공하고 각 기능들에 대해 모듈 라이브러리를 제공하고 있다.

모듈 설명
resilience4j-circuitbreaker CircuitBreaker 처리
resilience4j-bulkhead 병렬 작업 제한 관리
resilience4j-ratelimiter 요청 제한 관리
resilience4j-retry 재시도 관리
resilience4j-timelimiter 실행 시간 제한 관리
resilience4j-cache 캐시 처리

 

Resilience4j CircuitBreaker

필자가 관심있는 부분은 CircuitBreaker임으로 먼저 CircuitBreaker 부터 알아보고 나중에 다른 모듈들을 알아가보자.

CircuitBreaker에서 설정할 수 있는 옵션은 대략 아래와 같다. 디폴트값들이 있어서 필요한 부분만 설정하면 된다.

옵션 디폴트 값 설명
failureRateThreshold 50 % Circuit이 OPEN되는 실패율
slowCallDurationThreshold 60000 ms 느린 호출(slow call)로 판단하는 호출 시간
slowCallRateThreshold 100 Circuit이 OPEN되는 느린호출 비율(slow call rate)
permittedNumberOfCallsInHalfOpenState 10 HALF_OPEN 상태일 때 허용되는 호출 수
maxWaitDurationInHalfOpenState 0 ms HALF_OPEN 상태에서 대기하는 최대 시간
slidingWindowType COUNT_BASED CircuitBreaker 호출 결과를 기록하는데 사용되는 슬라이딩 윈도우 타입
(COUNT_BASED or TIME_BASED)
slidingWindowSize 100 슬라이딩 윈도우 크기
COUNT_BASED = array 크기
TIME_BASED =  초 
minimumNumberOfCalls 100 Circuit 동작시키기위한 최소한의 call 수
(실패 or 느린 호출을 계산하기 위해 필요한 최소한의 호출 수)
waitDurationInOpenState 60000 ms OPEN -> HALF_OPEN으로 변경되는 대기 시간
automaticTransitionFromOpenToHalfOpenEnabled false OPEN -> HALF_OPEN 자동 변경 여부
recordExceptions empty 실패로 측정하는 예외 리스트
(명시하면 명시하지 않은 예외는 성공으로 간주)
ignoreExceptions empty 실패로 처리하지 않을예외 리스트
recordFailurePredicate throwable -> true 특정 예외가 실패로 측정되도록 하는 커스텀예외
(기본값으로 모든 예외는 실패로 기록)
ignoreExceptionPredicate throwable -> false 특정 예외가 측정되지 않도록 하는 커스텀예외
(기본값으로 모든 예외는 무시되지 않음)

- COUNT_BASED 슬라이딩 윈도우 타입: 호출 횟수를 기반으로 슬라이딩 윈도우를 잡는 방법. N 크기의 circular array를 생성해서 해당 array의 실패률을 이용하여 circuit의 상태를 확정.

- TIME_BASED 슬라이딩 윈도우 타입: 시간을 기반으로 슬라이딩 윈도우를 잡는 방법. N 초의 슬라이딩 윈도우 크기라면 N 개의 circular array를 생성하고 버킷에 특정 초에 발생한 호출의 결과를 집계 및 저장하고 있고, 이를 초가 지남에 따라 밀어내는 방식.

 

아래와 같이 옵션을 설정했다고 했을 때 슬라이딩 윈도우 타입 별로 동작 샘플을 알아보자

  • slidingWindowSize = 5
  • minimumNumberOfCalls = 3
  • failureRateThreshold = 50(%)
  • slowCallDurationThreshold = 3000(ms)

1. COUNT_BASED

호출 순서 1 2 3 4 5 6
성공/실패 성공 실패 성공 성공 실패 실패
실패율(%) 0 1/2=50% 1/3=33% 1/4=25% 2/5=40% 3/5=60%
Circuit 상태 CLOSED CLOSED CLOSED CLOSED CLOSED OPEN
  • 실패 케이스: 예외 발생인 경우와 느린 호출(3000ms 이상) 인 경우
  • 2번째 호출에서 실패율이 50%가 되었지만 Circuit을 동작시키기위한 최소한의 call 수(minimumNumberOfCalls)가 3이 아니기 때문에 계속 CLOSED 상태를 유지 
  • 슬라이딩 윈도우 크기(slidingWindowSize)가 5임으로 6번째 호출이 들어오면 호출순서 2~6(5개)번째 호출을 기준으로 계산

 

2. TIME_BASED

호출 순서(초) 1 2 3 4 5 6
호출 횟수 1 0 3 2 4 2
실패 횟수 0 0 1 1 2 2
실패율(%) - - 1/4=25% 2/6=33% 4/10=40% 6/11=55%
Circuit 상태 CLOSED CLOSED CLOSED CLOSED CLOSED OPEN
  • 실패 케이스: 예외 발생인 경우와 느린 호출(3000ms 이상) 인 경우
  • 매 초마다 전체 호출 횟수 대비 실패 횟수로 실패율을 계산
  • 3초(minimumNumberOfCalls) 이후 부터 5초(slidingWindowSize) 이내의 실패율로 계산해서 6초에서는 2~6초의 실패율 계산 결과 55%가 되어 Cicuit OPEN 됨

 

 

SpringBoot  Resilience4j 적용

먼저 아래와 같이 의존성 설정을 한다. 필자는 SpringBoot3를 사용하고 있어서 resilience4j-spring-boot3를 설정했고, 추가적으로 aop, actuator를 선언해준다.

implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.1.0'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-aop'

 

application.yml 설정을 아래와 같이 해준다. 위에서 설명한 옵션을 설정하는 부분이다.

resilience4j:
  circuitbreaker:
    configs:
      default:  # 기본 config 명
        registerHealthIndicator: true
        # COUNT_BASED(마지막 N번의 호출 결과를 기반), TIME_BASED(마지막 N초의 결과를 기반) (호출 결과를 저장하고 집계하기 위해 slidingWindow 사용)
        slidingWindowType: COUNT_BASED 
        # COUNT_BASED: array크기, TIME_BASED: 초
        slidingWindowSize: 5 
        # circuit을 동작시키기위한 최소한의 call 수
        minimumNumberOfCalls: 5  
        slowCallRateThreshold: 100 
        slowCallDurationThreshold: 60000 
        failureRateThreshold: 50 # 실패율 임계값(50%)
        permittedNumberOfCallsInHalfOpenState: 3
        # 서킷의 상태가 Open 에서 Half-open 으로 변경되기전에 Circuit Break가 기다리는 시간[s](HALF_OPEN 상태로 빨리 전환되어 장애가 복구 될 수 있도록)
        waitDurationInOpenState: 20s  
    instances:
      circuit-sample-common: # circuitbreaker name
        baseConfig: default # 기본 config 지정
      circuit-sample-3000: 
        baseConfig: default 
        slowCallDurationThreshold: 3000 # 응답시간이 느린것으로 판단할 기준 시간 [ms]

 

 

이제 CircuitBreaker를 적용하는 Service 클래스를 작성해준다.

@Service
@Slf4j
public class CircuitBreakerService {

    /**
     * 응답 Delay 샘플
     * : application.yml 설정의 'slidingWindowSize', 'minimumNumberOfCalls', 'failureRateThreshold' 따라 6번째 호출부터 fallbackMethod 실행
     */
    @CircuitBreaker(name = "circuit-sample-3000", fallbackMethod = "fallback")
    public String slowCall(){

        long start = System.currentTimeMillis();
        try {
            Thread.sleep(5000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        log.info("[slowCall] call => {}ms", end - start);

        return "success";
    }

    /**
     * Exception 응답 샘플
     */
    @CircuitBreaker(name = "circuit-sample-common", fallbackMethod = "fallback")
    public String exceptionCall() {
        log.info("[exceptionCall] call!!");
        int i = 1/0;   // Exception 발생 시 즉시 fallbackMethod 실행 됨
        log.info("i {}", i);

        return "exception";
    }

    /**
     * CircuitBreaker Fallback 메소드
     */
    private String fallback(Throwable t) {
        log.info("[fallbackMethod] call!!");
        return "fallback";
    }
}

 

다음으로 테스트용 Controller를 작성해본다.

@RestController
@RequiredArgsConstructor
@RequestMapping("/circuitbreaker")
public class CircuitBreakerController {

    private final CircuitBreakerService circuitBreakerService;

    @GetMapping(value = "/slowcall")
    public String slowCall(){
        return circuitBreakerService.slowCall();
    }

    @GetMapping(value = "/exceptionCall")
    public String exceptionCall(){
        return circuitBreakerService.exceptionCall();
    }
}

 

설정 값들을 변경해가면서 테스트를 진행해보면 동작 방식을 이해하는데 도움이 될 것 같다.

 

참고:

https://velog.io/@haerong22/Spring-Cloud-를-이용한-MSA-4.-장애처리Resilience4j

https://mein-figur.tistory.com/entry/resilience4j-circuit-breaker-example

 

반응형

댓글