본문 바로가기
스프링클라우드

resilience4j - 서킷브레이커

by pius712 2024. 10. 5.

1. 컨셉 살펴보기

1.1 개념

서킷브레이커는 회로 차단기라는 뜻으로 소프트웨어에서는 호출 대상이 비정상적인 상태일 때, 작업이 실행되지 않도록 하는 장치를 의미한다.

여기서 호출 대상은 외부 시스템 api, 데이터베이스 등이 될 수 있다.

서킷브레이커는 호출의 성공 여부를 슬라이딩 윈도우를 통해 기록하여, 기준으로 정한 임계값을 넘어서는 경우 회로를 차단하여, 작업이 빠르게 실패하게 한다.

서킷브레이커는 크게 3가지 상태를 가지고 있다.

  • CLOSED
    • 회로가 차단된 상태로 호출 대상을 정삭적으로 호출할 수 있다.
  • OPEN
    • 회로가 열린 상태로, 실패율이 임계치를 넘겨서 더이상 호출 대상을 호출하지 못하는 상태이다.
  • HALF_OPEN
    • OPEN 상태에서 일정 시간이 지난 후에 전이되는 상태로, CLOSED 와 마찬가지로 호출가능한 상태이나 실패율 임계치를 넘는 경우 다시 OPEN 상태로 전이된다. 임계치보다 낮은 경우 CLOSED 상태로 전이된다.

1.2 슬라이딩 윈도우

서킷브레이커는 OPEN, CLOSED, HALF_OPEN 의 상태를 가지는 유한 상태머신으로 동작한다.

이러한 상태를 측정하기 위해 슬라이딩 윈도우라는 개념을 사용한다.

Count-based sliding window

window size 크기의 버킷이 생성되며, 해당 크기의 측정치를 저장한다.

 

Time-based sliding window

window size 크기의 버킷이 생성되며, 각 버킷은 매초 단위로 할당된다.

버킷에는 매초마다 task 호출의 결과를 저장한다.

1.3 실패율 계산 (failure rate, slow call rate)

서킷브레이커는 두 가지 상황에서 CLOSED → OPEN 상태로 전이된다.

  • failure rate 가 threshold 초과
  • slow call rate 가 threshold 초과

failure rate

failure rate 는 전체 실행된 task 중 예외 발생 비율을 의미한다.

이 비율에 설정된 threshold 를 초과하는 경우, OPEN 상태로 전이된다.

기본적으로는 모든 예외를 실패로 간주하나, record exception 옵션을 통해 특정 예외만 실패로 간주할 수 있도록 해준다.

slow call rate

slow call rate 는 예외가 발생하지 않더라도, 설정된 값 보다 느리게 처리되는 task 의 비율을 의미한다.

설정된 시간을 넘는 비율이 threshold 를 초과하는 경우, OPEN 상태로 전이된다.

실패율 계산을 최소한의 호출 수 설정을 통해, 일정 호출 수 이상이 되어야 계산하도록 할 수 있다.

예를들어 minimumNumberOfCalls 설정 값이 10인 경우, 총 호출 수가 9개이고 모두 실패하여도 실패율 계산을 하지 않기에 서킷브레이커가 OPEN 상태로 전이되지 않는다.

만약 threshold를 초과하여 OPEN 으로 전환되는 경우, 해당 task 호출은 CallNotPermittedException 이 발생한다. 일정 시간이 지난 후 서킷브레이커는 HALF_OPEN 상태로 전이된다.

HALF_OPEN 상태가 된 서킷브레이커는 permittedNumberOfCallsInHalfOpenState 수 만큼의 task 호출을 허용한다. 만약, 허용된 수 만큼 task 가 수행중에 추가 호출시 task 는 CallNotPermittedException 이 발생하여 거절된다.

참고로, sliding window 에는 CLOSED 상태일 때 결과를 저장하기 때문에, 다른 상태일 때는 실패율 계산에 포함되지 않는다.

또한 HALF_OPEN 상태에서는 permittedNumberOfCallsInHalfOpenState 에서 설정된 호출 수를 기준으로 실패율을 측정하고, OPEN 상태로 전이할지 CLOSED 상태로 전이할지 결정하게 된다.

2. 필요한 이유

Release 의 모든 것이라는 책에 보면, 서킷 브레이커가 필요한 이유에 대한 자세한 내용을 살펴볼 수 있다.

https://product.kyobobook.co.kr/detail/S000211502257

 

Release의 모든 것 | 마이클 나이가드 - 교보문고

Release의 모든 것 | 35년 경력 전문가의 경험이 담긴 소프트웨어 엔지니어링 베스트셀러로, 소프트웨어를 문제 없이 빠르게 출시할 수 있는 설계 방법에 초점을 맞춘 책입니다. 특히 사례 연구를

product.kyobobook.co.kr

 

서킷브레이커는 사용하는 이유는 여러가지가 있다.

종종 서킷브레이커의 주요 목적을 대상 서비스의 복구를 위한 것이라고 설명한다.

물론 이것도 맞지만 서킷브레이커의 주요 목적은 upstream 의 장애로 인한 downstream 의 연쇄적인 장애를 차단하고자 하는 목적이 더 크다.

즉, 서킷브레이커의 주요 목적은 우리의 시스템과 그에 의존하는 하위 시스템들의 연쇄적인 장애로부터 보호하고, 리소스를 낭비를 줄여 가용성을 유지하기 위한 것이 더 큰 목적이다.

2.1. 시스템을 연계 장애로 부터 보호한다.

빠른 실패 응답보다 느린 응답이 더 나쁘다.

MSA 아키텍처로 구성된 시스템의 경우 시스템 내부의 api 호출이 빈번하다.

일반적으로 호출 대상 서비스가 느리게 응답하는 경우, 우리의 서버 또한 느리게 응답하게 되고 이는 우리의 서버에 의존하는 서비스들에 전파가 된다.

결국, 느린 응답으로 스레드가 블록킹되면서 전체 시스템의 응답성이 떨어지게 된다.

이 경우, 서킷 브레이커를 통해 빠른 실패와 fallback 전략을 사용하면 전면 장애를 피할 수 있다.

2.2 리소스 낭비 방지

외부의 시스템이 장애가 난 상황에서 지속적인 요청을 하는 것은 여러모로 리소스 낭비다.

이 과정에서 발생할 수 있는 리소스 낭비에 대해 알아보자

  • CPU 자원 소모
    • api 요청시, 논블럭킹 방식으로 처리하지 않는다면 스레드가 블록된다. 그리고 이 스레드가 깨어나서 다시 작업을 하는 과정에서 CPU 사이클이 낭비된다.
  • 소켓 자원 소모
    • HTTP 통신을 한다는 것은 네트워크 하부에서 TCP 통신을 하게 된다. 즉, 외부의 서버와 소켓이 연결되어야 하는데, 소켓을 연결한다는 것은 메모리를 사용하는 것으로 리소스가 낭비된다.
  • 스레드 낭비
    • 실패할 요청을 처리하기 위해 이런 경우 스레드가 불필요하게 오래 사용된다. 빠르게 실패하고 스레드 풀에 스레드를 반납하는 것이 리소스 낭비를 줄일 수 있다.

2.2 통합 지점을 보호한다.

일반적으로 외부 대상 서비스의 api 가 실패했을 때, Retry 전략을 사용하고는 한다. 하지만, 일시적인 예외가 아니라 외부 서비스가 트래픽이 많아져서 대부분의 스레드가 작업중인 상황일 수도 있다.

이 경우, 실패를 Retry 를 통해 해결하려고 한다면 외부 서비스는 더 많은 부하가 생겨서 더 느린 응답, 더 많은 리소스 사용 등의 문제를 만들 수 있다.

3. 사용법

3.1 package

어노테이션 기반으로 서킷브레이커를 동작시키기 위해서는 aop 라이브러리를 설치해야한다.

resilience4j 를 직접 설치할 수도 있고, spring-cloud-starter-circuitbreaker 를 설치할 수도 있다.

spring-cloud-starter-circuitbreaker 는 circuitbreaker 를 추상화한 라이브러리로 resilience4j 와 같은 구현체를 쉽게 변경할 수 있게 해준다.

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-aop")
    implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j")
}

하지만 어노테이션을 통해 기능을 사용하는 경우에 어노테이션을 제공해주지 않아서, resilience4j 라이브러리를 직접 import 하기 때문에 추상화의 이점을 얻기는 어려워 보인다.

당근마켓에서 이러한 한계점에 대한 개선 작업에 대한 영상을 제공하고 있으니 참고해보자.

https://www.youtube.com/watch?v=ThLfHtoEe1I

 

3.2 설정

resilience4j 에서는 CircuitBreakerRegistry 를 통해 서킷브레이커의 인스턴스를 관리한다.

프로그래밍 방식을 통해 설정을 할 수도 있고, application.yml 을 통해서 설정을 할 수도 있다.

아래는 application.yml 을 통한 설정이다.

구체적인 설정 값들은 별도로 설명하고자한다. 여기서는 instance 에 대해 살펴보자.

  • defaults 를 통해, 기본 설정을 추가한다.
  • 추가 인스턴스를 통해 defaults 의 설정을 오버라이딩한다.
resilience4j.circuitbreaker:
    instances:
      defaults:
        sliding-window-type: count_based
        failure-rate-threshold: 50
        minimum-number-of-calls: 5
        automatic-transition-from-open-to-half-open-enabled: true
        wait-duration-in-open-state: 5s
        permitted-number-of-calls-in-half-open-state: 3
        sliding-window-size: 10
        record-exceptions:
          - java.lang.RuntimeException
      test:
					# overriding 가능
			test2:
					# overriding 가능

3.3 어노테이션 기반 예시

사용방법은 비교적 간단하다.

@CircuitBreaker 어노테이션을 통해 서킷브레이커를 적용할 수 있다.

name 에는 circuit breaker 인스턴스 (위 application.yml 에서 설정한) 를 지정할 수 있다.

만약 매칭되는 인스턴스가 없다면, AOP 가 실행되는 시점에 해당 인스턴스가 기본 설정값을 기반으로 생성된다.

@Service
class CircuitBreakerService(
) {

    private val logger = LoggerFactory.getLogger(javaClass)
    @CircuitBreaker(name = "test", fallbackMethod = "fallback")
    fun test(test:String): String {
        logger.info("test method called")
        if(test == "test") {
            throw RuntimeException("test")
        }
        return "test"
    }

    fun fallback(e: Exception):String {
        logger.error("fallback", e)
        return "fallback"
    }
}

3.4 event consume

circuit breaker 를 사용하면, 여러가지 이벤트가 발생하게 되고 해당 이벤트에 대한 핸들러를 등록할 수 있다.

  • onSuccess: task가 정상호출
  • onError: task 에서 record exception 발생
  • onStateTransition: 서킷브레이커의 상태 전이
  • onIgnoredError: task 에서 ignored exception 발생
  • onCallNotPermitted: 서킷브레이커가 OPEN 상태일 때, task 호출
  • onReset: 슬라이딩 윈도우의 데이터가 모두 제거되었을 경우 호출 (circuitBreaker 의 reset 메서드 호출)
@Configuration
class CircuitBreakerConfig {

    private val logger = LoggerFactory.getLogger(javaClass)
    @Bean
    fun circuitBreakerEventListener(circuitBreakerRegistry: CircuitBreakerRegistry):CircuitBreaker {
        val circuitBreaker: CircuitBreaker = circuitBreakerRegistry.circuitBreaker("test")

        // 상태 전환 이벤트 리스너
        circuitBreaker.eventPublisher.onSuccess { event->
            logger.info("CircuitBreaker 성공: ${event}")
        }.onError { event->
            logger.error("CircuitBreaker 에러 발생: ${event.throwable.message}")
        }.onStateTransition { event ->
            logger.info("CircuitBreaker 상태 전환: ${event.stateTransition}")
        }.onIgnoredError { event->
            logger.error("CircuitBreaker 무시된 에러 발생: ${event.throwable.message}")
        }.onCallNotPermitted { event->
            logger.error("CircuitBreaker 호출 금지: ${event}")
        }.onReset { event->
            logger.info("CircuitBreaker 리셋: ${event}")
        }
        return circuitBreaker
    }
}

참고로, 만약 instance 를 등록하지 않는다면 서버가 기동될 때 circuitBreakerRegistry 가 없기 때문에,

이벤트를 등록할 수 없다.

4. 설정 살펴보기

아래에서 세부적인 설정들을 살펴보려고한다.

소제목의 괄호는 디폴트 값을 표기한다.

- slidingWindowType (COUNT_BASED)

서킷 브레이커의 슬라이딩 윈도우는 두가지 타입이 있다.

  • COUNT_BASED
    • 이 경우, slidingWindowSize 는 호출 수를 의미한다.
  • TIME_BASED
    • 이 경우, slidingWidnowSize 는 초를 의미한다.

- slidingWindowSize(100)

슬라이딩 윈도우의 크기를 나타내며, 서킷이 CLOSED 상태일 때 task 호출 결과를 저장한다.

 

- minimumNumberOfCalls(100)

실패율 (failure rate, slow call rate) 를 계산하기 위한 최소한의 task 호출 수를 정의한다.

예를들어, 실패율이 50, 슬라이딩 윈도우의 사이즈가 10이라고 가정해보자.

이 경우, 최초 5개의 실패만 발생해도 실패율을 충족한다.

하지만, minimumNumberOfCalls 가 7이라면 5건의 실패 발생시 실패율을 계산하지 않는다.

따라서 이 경우에는 CLOSED 상태가 유지된다.

 

- waitDurationInOpenState(60_000)

서킷브레이커가 OPEN → HALF_OPEN 상태로 넘어가기 위해 대기하는 시간이다.

즉, 기본값 사용시 OPEN 상태로 전이된 후 60초간 호출이 정지된다.

이때 주의할 점이, 마지막 호출 시점을 기준으로 대기하는 것이 아니라 OPEN 상태로 전이된 후 해당 시간만큼 지나면 상태가 전이된다.

 

- failureRateThreshold(50)

실패율 임계치를 퍼센트로 표현한다.

만약 실패율이 이 값보다 큰 경우, 서킷 브레이커가 OPEN 상태로 전이된다.

 

- slowCallRateThreshold (100)

slowCallDurationThreshold 에 지정된 시간보다 느리게 응답하는 비율이 임계값을 넘는 경우,

서킷브레이커가 OPEN 상태로 전이된다.

 

- slowCallDurationThreshold (60_000[ms])

task 실행이 느리다고 판단되는 기준을 지정할 수 있다.

 

- recordExceptions(empty)

해당 옵션에 명시된 예외와 그 하위 타입의 예외는 발생시 실패율에 영향을 준다.

만약, 해당 옵션에 예외를 명시하는 경우, 명시되지 않은 모든 예외 발생은 성공으로 계산된다.

 

- ignoreExceptions(empty)

해당 옵션에 명시된 예외와 그 하위 타입의 예외는 발생하여도 실패나 성공으로 기록되지 않는다.

즉, 슬라이딩 윈도우에 영향을 주지 않는다.

참고문서

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

https://product.kyobobook.co.kr/detail/S000211502257

 

'스프링클라우드' 카테고리의 다른 글

spring cloud gateway - 기능 간단 살펴보기  (1) 2023.12.30