모종닷컴

Circuit Breaker Pattern 그리고 이를 스프링에 적용해보기 본문

Programming/Spring

Circuit Breaker Pattern 그리고 이를 스프링에 적용해보기

모종 2022. 11. 12. 16:47
반응형

Circuit Breaker는 무엇이며 어떠한 상황에서 사용될까?

circuit breaker는 이름에서 알 수 있듯이 회로를 차단하는 기능입니다. 주로 외부 콜에 의존하는 부분에 사용합니다. 예를 들어 A와 B 서버가 존재한다고 가정해보겠습니다. A서버에서는 B서버의 API에 의존하고 있는 형태일 때 만약 B서버에 과부하가 걸려 응답을 제시간에 못주게 되는 경우 A서버에서의 API 호출은 응답을 기다리다가 모두 실패하게 될 것입니다. 이렇게 B서버의 장애는 A서버 외에도 B서버를 사용하고 있는 모든 서버에 장애가 전파되게 됩니다. 이때 이런 생각을 해보게 됩니다.

"어차피 지금 B서버의 요청은 모두 timeout이 나게 될텐데 A서버가 이 요청을 위해 리소스를 계속 낭비하고 있어야 할까?"

예를 들어 A가 데이터베이스 커넥션을 하나 물고 있는 상황에서 B서버를 호출하는 방식이라면 B서버가 정상화되기까지 A서버의 데이터베이스 커넥션 & 스레드 & 메모리 등의 리소스는 지정한 timeout 시간 동안 계속 이용하지 못하게 될 테니까요(실패 시 재시도하는 로직까지 있다면 더욱 상황이 악화되겠네요..). 이렇게 A서버가 리소스를 계속해서 낭비하다 보면 어느새 A서버까지 장애가 일어나게 되겠죠.

이런 상황에 대해 대처하기 위한 방법 중 하나가 바로 Circuit Breaker Pattern입니다. A서버에서 B서버의 API 호출 결과를 저장하고 모니터링하고 있다가 호출 성공률이 일정 퍼센트 이하로 떨어지면 circuit breaker가 동작하게 되고, A서버에서는 B서버의 호출하는 부분이 차단되게 되면서 즉각적인 실패 응답을 받을 수 있게 되는 것이죠.

요약

circuit breaker는 어느 한 서비스의 장애가 시스템의 다른 서비스까지 전파되지 않도록 차단하는 기술입니다

Circuit Breaker 사용해보기

서킷브레이커가 무엇인지 간단하게 알아보았으니 이것을 스프링 애플리케이션에 적용하는 법에 대해 알아보겠습니다. 

dependency 추가

implementation 'io.github.resilience4j:resilience4j-spring-boot2:1.7.1'
implementation 'org.springframework.boot:spring-boot-starter-actuator'

프로퍼티 설정

# CircuitBreaker
resilience4j:
  circuitbreaker:
    configs:
      default:
        registerHealthIndicator: true
    instances:
      circuitBreakerTest:
        # https://resilience4j.readme.io/docs/circuitbreaker#create-and-configure-a-circuitbreaker
        registerHealthIndicator: true
        slidingWindowType: COUNT_BASED # 슬라이딩 윈도우 타입
        slidingWindowSize: 10 # 슬라이딩 윈도우 사이즈
        minimumNumberOfCalls: 10
        waitDurationInOpenState: 1m # OPEN 상태를 얼마나 유지시킬지
        automaticTransitionFromOpenToHalfOpenEnabled: true # OPEN -> HALF OPEN 상태로 수동 조정없이 가능하게할지
        failureRateThreshold: 50 # 실패 임계율 50%
        permittedNumberOfCallsInHalfOpenState: 5 # half_open 상태가 되었을 때 허용할 요청 개수. 이때의 요청들로 다시 실패율 계산.

# Actuator
management:
  health:
    circuitbreakers:
      enabled: true
  endpoint:
    health:
      show-details: always

위 설정을 간략하게 설정하자면 10개의 요청 결과를 저장하고 있고, 10개의 요청 결과에 대해서 실패율이 50% 이상이 되는 순간 CircuitBreaker의 상태가 OPEN(=회로 차단 on)으로 바뀝니다. 차단은 1분 동안 지속되며 그 후 상태가 HALF_OPEN 상태가 되는데 이 상태에서 5개의 요청을 받아보고 이 5개의 요청의 실패율이 50% 이상이 된다면 다시 OPEN 상태로 바뀌고, 만약 5개의 요청의 실패율이 50% 미만이라면 CircuitBreaker 상태가 다시 CLOSE(=회로 차단 off)로 돌아가게 됩니다.

자세한 설명은 https://resilience4j.readme.io/docs/circuitbreaker 에서 확인해보시기 바랍니다.

코드 작성하기

@Service
class OrderService(
    private val mockService: MockService,
    private val circuitBreakerRegistry: CircuitBreakerRegistry
) {
    private val circuitBreaker: CircuitBreaker by lazy {
        circuitBreakerRegistry.circuitBreaker("circuitBreakerTest")
    }

    fun order(): String {
        return circuitBreaker.executeSupplier { mockService.request() }
    }
}

@Service
class MockService(
    var errorFlag: Boolean = false
) {
    fun request(): String {
        return if(!errorFlag) {
            "OPEN"
        } else {
            throw IllegalStateException("닫혀있음.")
        }
    }
}

설명

- OrderService는 MockService 응답에 의존.
- MockService에는 errorFlag라는 flag가 존재하고 이 flag가 TRUE일 경우 예외를 뱉도록 합니다. 
- mockService의 예외는 orderService에게 전파됩니다.

 

private val log = KotlinLogging.logger {  }

@RestController
@RequestMapping("/api/circuitbreaker")
class CircuitBreakerTestApiController(
    private val mockService: MockService,
    private val orderService: OrderService
) {
    /**
     * 회로가 차단되었을 때는 CircuitBreaker가 CallNotPermittedException 예외를 던지게 되는데 이 예외가 날 경우 여기서 핸들링합니다.
     */
    @ExceptionHandler(value = [CallNotPermittedException::class])
    fun fallbackHandle(e: CallNotPermittedException): ResponseEntity<String> {
        return ResponseEntity.badRequest().body("NOT_PERMITTED")
    }

    /**
     * endpoint : /api/circuitbreaker/test?success={}&fail={}
     *
     * @param success 지정한 개수만큼 성공하는 호출
     * @param fail 지정한 개수만큼 실패하는 호출
     * */
    @PostMapping("/test")
    fun composite(@RequestParam success: Int, @RequestParam fail: Int): ResponseEntity<String> {
        mockService.errorFlag = false
        call(success)

        mockService.errorFlag = true
        call(fail)

        return ResponseEntity.ok("OK")
    }

    private fun call(cnt: Int) {
        repeat(cnt) {
            try {
                orderService.order()
            } catch (e: IllegalStateException) {
                log.warn { "order() Request Fail." }
            }
        }
    }
}

테스트해보기

정상 요청 10개 호출 & Circuit 상태 보기

먼저 정상적인 요청 10개를 호출하고 Spring Actuator를 통해 circuitBreaker의 상태를 보도록 하겠습니다. POST 메서드로 해당 엔드포인트를 호출해줍니다.

POST http://localhost:{{port}}/api/circuitbreaker/test?success=10&fail=0

그 후에 GET 요청으로 /actuator/health를 호출해봅니다. 저는 브라우저에서 호출해주었습니다.

GET http://localhost:{{port}}/actuator/health

10개의 요청이 모두 성공하였기에 실패율(=failureRate)은 0%입니다. 실패율이 50%를 넘지 않았기에 상태 역시 CLOSED입니다. 

실패 요청 한 개 호출해보기 & Circuit 상태 보기

이 상태에서 이제 실패 요청 하나를 날리면 위 값들이 어떻게 바뀌는지 보겠습니다. success를 0 fail을 1로 놓고 test 엔드포인트를 호출합니다.

POST http://localhost:{{port}}/api/circuitbreaker/test?success=0&fail=1

호출을 했으면 다시 actuator를 봅니다. 실패 요청(failedCalls)이 1로 바뀌었고, 가장 최근 10개 중 1개가 실패했으니 실패율은 10%가 되었습니다.

실패 요청 4개를 더 호출해보고 Circuit 상태 보기 & 회로가 차단된 상황에서 요청 날려보기

이전과 같은 방식으로 실패 요청을 4개 날려보고 상태를 보겠습니다.

실패율이 50퍼가 되자마자 circuit breaker이 상태가 OPEN으로 바뀌어서 회로가 차단되었습니다. 이 상태에서 성공 요청 하나를 날려볼까요?

여기서 제대로 된 테스트를 하기 위해서는 서킷이 반드시 OPEN 상태가 되었을 때 해야 하는 것입니다. 설정한 1분이라는 시간이 지나면 HALF_OPEN으로 바뀌기 때문입니다. 서킷이 OPEN 된 상태에서 요청이 어떻게 되는지 좀 느긋하게 보고 싶다고 하신다면 프로퍼티 설정의 waitDuratonInOpenState 값을 충분하게 설정해주시기 바랍니다.

400 ERROR와 함께 HTTP Body에 NOT_PERMITTED가 세팅되어서 나왔습니다. 서킷이 Open 된 상태이기 때문입니다. 

HALF_OPEN 상태에서 (성공 요청 3개, 실패 요청 2개) 날려보기 or (성공 요청 2개 실패 요청 3개) 날려보기.

서킷이 OPEN 된 상황에서 위에서 설정한 1분이라는 시간이 지나면 HALF_OPEN 상태가 됩니다. 위에서 프로퍼티 설정할 때 HALF_OPEN 상태에서는 최근 요청의 5개만 보도록 설정(=permittedNumberOfCallsInHalfOpenState)하였습니다.

1분 정도 기다렸다가 성공 요청 3개, 실패 요청 2개를 날리게 되면 실패율이 40%이므로 Circuit Breaker의 상태가 CLOSED가 될 것이고, HALF_OPEN 상태에서 실패율이 50% 넘도록 실패 요청 3개 성공 요청 2개를 날리면 다시 OPEN 상태로 바뀌게 됩니다.

 

포스팅 마무리

이번 글에서 CircuitBreaker란 무엇인지 언제 사용해야 하는지, 스프링 프레임워크 적용 방법 및 테스트 등을 다루었습니다. 오늘 제가 서킷을 적용한 건 카운트 기반의 서킷브레이커인데 이 외에 시간 기반 서킷도 존재합니다. 시간이 된다면.. 아니 시간을 내서라도 https://resilience4j.readme.io/docs/circuitbreaker 공식 문서를 한번 쭉 보면 좋겠습니다. 글이 짧으면서도 핵심 내용이 잘 들어가 있어서 이해하는데 많은 도움이 될 것입니다. 

글이 길어지는 듯하여 여기서 포스팅을 마치는데 조만간 Circuit Breaker의 상태 변화에 따른 이벤트 리스너 추가 및 서킷의 상태를 수동으로 변경하는 법 및 서킷브레이커를 리셋하는 방법을 쓰려고 합니다. 관심이 있다면 후속 포스팅도 좋은 정보가 될 것입니다. 

글 정리가 제대로 되지 않아서 보기 힘들었을 텐데 읽어주셔서 감사합니다. 

반응형