Kotlin Spring + Mybatis + Mysql 샘플 데이터 토이프로젝트 -8 Resilience4J Circuit Breaker 적용

선종우·2024년 8월 26일
0

Spring 노트

목록 보기
8/10
  • 서킷브레이커에 대해 알아보고 프로젝트에 적용해 보았다.
    https://resilience4j.readme.io/docs/circuitbreaker
  • 서킷브레이커 3개 상태
    • CLOSED : 정상상태
    • OPEN : 장애 발생 상태, 해당 기능을 중단시킨다
    • HALF_OPEN : Open상태에서 일정시간 지나고 일부 허용된 요청만 허용되는 상태. 만약 요청이 성공하면 상태는 CLOSED상태가 된다. 요청이 다시 실패하면 OPEN 상태가 된다. Half-Open 상태는 회복중인 시스템으로 다시 요청이 몰려 장애가 발생하는 상황을 막는데 유용함. 완충 역할을 하는 상태이다
      AZURE CB 설명
  • 서킷 브레이커는 Sliding window 내에서 실패율이 일정이상되면 CLOSE -> OPEN으로 변경된다
    • 아래 그림은 Sliding Window 사이즈가 10인 상황에서 4번 실패한 경우 임계점을 넘었다. 서킷 브레이커를 설정했다면 CLOSE -> OPEN 상태가 된다

https://dzone.com/articles/a-new-era-of-spring-cloud

  • SlidingWindow는 CountBase와 TimeBase로 구분할 수 있다

    • CountBase : Window size내 x개 이상 실패하면 OPEN으로 변경
    • TimeBase : 일정 시간 내 실패율이 임계치 이상이면 OPEN으로 변경
  • 서킷 브레이커 적용을 위한 기본 설정

resilience4j:
  circuitbreaker:
    configs:
      default:
        slidingWindowType: COUNT_BASED
        minimumNumberOfCalls: 1                                  # 최소 7번까지는 무조건 CLOSE로 가정
        slidingWindowSize: 4                                    # 10개 요청 단위
        waitDurationInOpenState: 10s                             # open -> halfopen 가는데 필요한 시간

        failureRateThreshold: 40                                 # slidingWindowSize 중 몇 % 장애 시 open으로 만들 것인지?

        slowCallDurationThreshold: 3000                          # 몇 ms 동안 요청이 처리되지 않으면 실패로 간주할 것인지?
        slowCallRateThreshold: 60                                # slidingwindow중 몇 %가 slowcall이면 open으로 간주할 것인지?

        permittedNumberOfCallsInHalfOpenState: 5                 # half-open 상태에서 5번까지는 close로 가기위해 호출한다
        automaticTransitionFromOpenToHalfOpenEnabled: true       # open 상태에서 자동으로 half-open으로 갈 것인가?

        eventConsumerBufferSize: 10                              # actuator를 위한 이벤트 버퍼 사이즈

        recordExceptions:
          - com.example.exception.ExternalApiException
        ignoreExceptions:                                        # fallback은 실행된다
          - java.lang.IllegalStateException

    instances:
      simpleCircuitBreakerConfig:
        baseConfig: default
  • 서킷브레이커가 적용된 서비스 코드
@Service
class ExternalCBApieService(
    private val externalClient: ExternalClient
) : Log {
    companion object {
        const val DEFAULT_CB_CONFIG = "simpleCircuitBreakerConfig"
    }

    @CircuitBreaker(name = DEFAULT_CB_CONFIG, fallbackMethod = "fallBack")
    fun requestApi(param: String): String {
        return when (param) {
            "OPEN" -> externalClient.requestEx(param)
            "CLOSE" -> externalClient.requestEx(param)
            "TIME_OUT" -> externalClient.timeout(param)
            else -> throw IllegalArgumentException()
        }
    }

    private fun fallBack(param: String, ex: ExternalApiException): String {
        log.info("api response error, state is CLOSE -> OPEN")
        return ex.message ?: ""
    }

    private fun fallBack(param: String, ex: CallNotPermittedException): String {
        log.info("CB state is open, api is not permitted")
        return ex.message ?: ""
    }
}
  • 정상 상태에서 예외가 발생한 경우(CLOSE -> OPEN)에는 RecordException으로 등록한 ExternalApiExcetion fallback이 적용된다.
  • OPEN 상태에서 해당 API로 다시 요청한 경우 CallNotPermittedException이 발생한다.
  • 아래와 같이 로깅설정을 할 수 있다
@Configuration
class ResilienceConfig : Log {
    @Bean
    fun circuitBreakerEventConsumer(): RegistryEventConsumer<CircuitBreaker> {
        return object : RegistryEventConsumer<CircuitBreaker> {
            override fun onEntryAddedEvent(entryAddedEvent: EntryAddedEvent<CircuitBreaker>) {
                val eventPublisher = entryAddedEvent.addedEntry.eventPublisher

                eventPublisher.onEvent { event -> log.info("{}", event) }
                eventPublisher.onCallNotPermitted { event -> log.info("{}", event) } // open 상태에서 요청이 들어온 경우
                eventPublisher.onStateTransition { event -> log.info("{}", event) } // state가 변경될 경우 // 다른 서버로 장애 전파할 때 사용할 수 있음
                eventPublisher.onFailureRateExceeded { event -> log.info("{}", event.eventType) }
            }

            override fun onEntryRemovedEvent(entryRemoveEvent: EntryRemovedEvent<CircuitBreaker>) {
                TODO("Not yet implemented")
            }

            override fun onEntryReplacedEvent(entryReplacedEvent: EntryReplacedEvent<CircuitBreaker>) {
                TODO("Not yet implemented")
            }
        }
    }
}
2024-08-27T00:05:05.875+09:00  INFO 1908 --- [demo] [nio-8080-exec-1] ResilienceConfig$$SpringCGLIB$$0         : 2024-08-27T00:05:05.875944400+09:00[Asia/Seoul]: CircuitBreaker 'simpleCircuitBreakerConfig' changed state from CLOSED to OPEN

0개의 댓글