[식구하자_MSA] API Gateway 제대로 쓰고 있는 거 맞아? - 2편: Circuit Breaker로 장애 전파 막기

이민우·2024년 8월 14일
4

🍀 식구하자_MSA

목록 보기
20/21

배경


이전 포스팅에 이어 식구하자 프로젝트에서 마이크로서비스 아키텍처(MSA)의 안정성을 강화하기 위해 Gateway 서버에 Resilience4j를 활용하여 서킷브레이커를 적용하는 과정을 소개하고자 합니다.
위에 그림처럼 저희가 서비스가 운영하는 과정에서 앞으로 발생할 수있는 예외를 안다면 더할 나위없이 좋겠지만 , 현실은 그렇게 녹록치 않습니다,,,,.

특히 MSA 환경에서는 특정 서버의 장애가 다른 서버로 전파될 수 있는 위험이 있습니다. 이러한 문제는 CircuitBreaker 패턴을 사용하여 효과적으로 해결할 수 있습니다.
기존에 마이크로서비스 간 동기 호출 시 CircuitBreaker 패턴을 적용하여 개별 서비스 수준에서 장애 전파를 방지했지만, 전체 시스템 차원의 중앙 집중식 장애 전파 방지 대책은 부재했습니다. 서킷 브레이커를 게이트웨이 서버에 적용함으로써 중앙에서 트래픽을 제어할 수 있게 되어, 서비스의 전반적인 안정성을 크게 향상시킬 수 있습니다.
이 포스팅에서는 Spring Cloud Gateway와 Resilience4j를 활용하여 이러한 중앙 집중식 서킷 브레이커 패턴을 구현하는 과정을 설명해보도록 하겠습니다!

🚨 CircuitBreaker란?


CircuitBreaker는 마이크로서비스 아키텍처(MSA)에서 중요한 역할을 하는 장애 관리 패턴입니다 (유사 전기 회로의 차단기).

🤷‍♂️ 왜 필요한가요?

Circuit Breaker가 필요한 이유는, 누전 차단기가 전기 사고가 발생하기 전에 전기를 미리 차단하는 것과 동일하게, 문제가 있는 마이크로서비스로의 트래픽을 차단하여 전체 서비스가 느려지거나 중단되는 것을 미리 방지합니다.

⚙️ 주요 기능:

  1. 문제 감지: 서비스 간 호출에서 발생하는 문제를 신속하게 감지합니다.
  2. 장애 전파 방지: 문제가 있는 서비스로의 호출을 차단하여 다른 서비스로 장애가 확산되는 것을 막습니다.
  3. 시스템 안정화: 장애 발생 시 빠른 복구를 돕고, 사용자 경험을 개선합니다.

💬 작동 예시

멤버 서비스가 채팅 서비스를 호출하는 상황을 가정해봅시다. 만약 쿠폰 서비스가 지속적으로 실패한다면, CircuitBreaker는 '열린(Open)' 상태가 되어 쿠폰 서비스로의 호출을 차단합니다. 이를 통해:

  • 쿠폰 서비스의 부하를 줄입니다.
  • 서비스 복구를 위한 시간을 확보합니다.
  • 다른 서비스들이 불필요하게 대기하는 상황을 방지합니다.

CircuitBreaker의 주요 상태:

  1. CLOSED (닫힘):
    • 정상 상태
    • 모든 요청이 대상 서비스로 전달됨
  2. OPEN (열림):
    • 장애 상태
    • 모든 요청이 즉시 차단되어 대상 서비스로 전달되지 않음
  3. HALF-OPEN (반열림):
    • 회복 테스트 상태
    • 제한된 수의 요청만 허용하여 서비스 상태를 확인
    • 결과에 따라 CLOSED 또는 OPEN 상태로 전환

이러한 메커니즘을 통해 CircuitBreaker는 시스템의 안정성을 높이고, 장애 상황에서도 전체 시스템의 가용성을 유지하는 데 중요한 역할을 합니다.

✅ Circuit Breaker 동작 원리


Circuit Breaker의 동작 원리는 다음과 같습니다:

  1. 슬라이딩 윈도우:
    • Circuit Breaker는 호출 결과를 저장하고 분석하기 위해 슬라이딩 윈도우를 사용합니다.
    • 두 가지 유형의 슬라이딩 윈도우가 있습니다:
      • 횟수 기반: 최근 N번의 호출 결과를 기록
      • 시간 기반: 최근 N초 동안의 호출 결과를 기록
  2. 상태 전환:
    • CLOSED (정상 상태) → OPEN (차단 상태):
      • 느린 호출 비율이나 실패 비율이 설정된 임계값을 초과할 때 발생
      • 최소 호출 수가 충족된 후에만 이 전환이 고려됨
    • OPEN → HALF-OPEN (부분 개방 상태):
      • 일정 시간이 지나면 HALF-OPEN 상태로 전환
      • 제한된 수의 요청만 허용하여 서비스 상태를 테스트
  3. 예외 처리:
    • 기본적으로 모든 예외는 실패로 간주됨
    • 특정 예외만 실패로 처리하거나, 일부 예외를 무시하도록 설정 가능
  4. 요청 처리:
    • OPEN 상태: 모든 요청에 대해 CallNotPermittedException 발생
    • HALF-OPEN 상태: 제한된 수의 요청만 허용, 나머지는 예외 발생
    • 결과에 따라 다시 CLOSED 또는 OPEN 상태로 전환
  5. 특별 상태:
    • DISABLED: 서킷 브레이커 비활성화, 모든 요청 허용
    • FORCED_OPEN: 강제로 서킷을 열어 모든 요청 거부
  6. 스레드 안전성:
    • 상태는 AtomicReference에 저장되어 동시성 문제 방지
    • 상태 업데이트는 atomic 연산으로 처리
    • 요청 기록 및 분석은 동기적으로 처리

📌 적용

의존성 추가

서킷브레이커를 사용하기 위해서는 의존성을 추가해야 합니다. 서킷브레이커를 구현하고자 하는 시스템의 구현에 따라서 서로 다른 의존성을 제공하며, Getting Started 페이지에서 확인할 수 있습니다!

저는 아래 의존성을 추가했습니다:

implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j'

주요 설정 정보


spring:
  cloud:
    gateway:
      default-filters:
        - name: CircuitBreaker
          args:
            name: defaultCircuitBreaker
            fallbackUri: forward:/fallback

resilience4j:
  circuitbreaker:
    configs:
      default:
        register-health-indicator: true  # 서킷 브레이커의 상태를 헬스 인디케이터로 등록
        allow-health-indicator-to-fail: false  # 헬스 인디케이터가 실패할 수 있는지 여부
        sliding-window-type: COUNT_BASED  # 슬라이딩 윈도우 타입: COUNT_BASED는 호출 수를 기준으로 함
        sliding-window-size: 10  # 슬라이딩 윈도우 크기: 10개의 호출
        minimum-number-of-calls: 10  # 서킷 브레이커가 동작하기 위한 최소 호출 수
        failure-rate-threshold: 50  # 실패율 임계값 (%)
        slow-call-rate-threshold: 50  # 느린 호출 비율 임계값 (%)
        slow-call-duration-threshold: 10s  # 느린 호출의 기준 시간 (초)
        wait-duration-in-open-state: 10s  # 서킷 브레이커가 오픈 상태에서 유지되는 시간 (초)
        automatic-transition-from-open-to-half-open-enabled: false  # 오픈 상태에서 반 오픈 상태로 자동 전환 여부
        permitted-number-of-calls-in-half-open-state: 5  # 반 오픈 상태에서 허용되는 호출 수
        record-exceptions:  # 서킷 브레이커가 예외로 간주할 예외 클래스들
          - java.util.concurrent.TimeoutException  # 타임아웃 예외
          - org.springframework.cloud.gateway.support.NotFoundException  # NotFound 예외
          - io.github.resilience4j.circuitbreaker.CallNotPermittedException  # 서킷 브레이커가 호출을 허용하지 않는 예외

    instances:
      defaultCircuitBreaker:
        baseConfig: default  # 기본 설정을 상속받음
        failure-rate-threshold: 50  # 실패율 임계값을 50%로 설정

서킷브레이커의 주요 설정값은 아래와 같습니다:

  • failureRateThreshold: 호출 실패율에 대한 임계값, 슬라이딩 윈도우 상에서 임곗값보다 실패율이 높아지면 상태가 OPEN 됩니다. 기본값은 50입니다.
  • slidingWindowSize: 슬라이딩 윈도우의 크기를 설정합니다. 기본값은 100입니다.
  • slidingWindowType: 슬라이딩 윈도의 형태를 COUNT_BASED로 설정할지, TIME_BASED로 설정할지 결정합니다. 타입에 따라 slidingWindowSize의 숫자가 의미하는 것이 호출 횟수 또는 시간이 됩니다. 기본값은 COUNT_BASED입니다.
  • `

waitDurationInOpenState: OPEN상태에서HALF_OPEN` 상태로의 전환을 기다리는 시간을 설정합니다. 기본값은 60초입니다.

  • automaticTransitionFromOpenToHalfOpenEnabled: OPEN 상태에서 자동으로 HALF_OPEN 상태로 전환할지 여부를 설정합니다. 기본값은 true입니다.

추가로)

CircuitBreaker의 instances 섹션에서는 여러 개의 CircuitBreaker 인스턴스를 정의할 수 있습니다. 각 인스턴스는 서로 다른 서비스나 API 엔드포인트에 대해 독립적으로 설정될 수 있습니다.

예를 들어:

instances:
  defaultCircuitBreaker:
    baseConfig: default
    failure-rate-threshold: 50
  serviceACircuitBreaker:
    baseConfig: default
    failure-rate-threshold: 30
  serviceBCircuitBreaker:
    baseConfig: default
    failure-rate-threshold: 40

이렇게 설정하면, 각 서비스나 API에 대해 다른 CircuitBreaker 설정을 적용할 수 있습니다. 서비스의 특성, 중요도, 예상되는 부하 등에 따라 각 인스턴스의 파라미터를 조정할 수 있습니다.

이를 통해 각 서비스에 맞는 최적화된 CircuitBreaker 동작을 구현할 수 있으며, 시스템 전체의 안정성과 회복력을 향상시킬 수 있습니다.

📌 circuitbreakes 등록됐는지 확인

[서버URL]/actuator/circuitbreakers 요청을 하면 등록된 서킷브레이커를 확인할 수 있습니다!

📌 fallback 설정 및 관련 예외

위에 yml을 설정을 보시면

서킷브레이커의 fallback 동작은 서비스 장애 시 사용자에게 대체 응답을 제공하는 메커니즘입니다. YAML 설정의 fallbackUri: forward:/fallback은 문제 발생 시 요청을 /fallback 엔드포인트로 보내도록 지시합니다.

/fallback 엔드포인트는 컨트롤러에서 처리됩니다.

@RequestMapping("/fallback")
    public Mono<ResponseEntity<String>> fallback(ServerWebExchange exchange) {
        Throwable exception = exchange.getAttribute(ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR);
        log.info("예외: {}", exception != null ? exception.getMessage() : "Unknown error");

        return Mono.just(exception)
                .map(ex -> {
                    if (ex instanceof NotFoundException) {
                        return new ResponseEntity<>("내부 서버 오류가 발생했습니다. 관리자에게 문의해 주세요.", HttpStatus.SERVICE_UNAVAILABLE);
                    } else if (ex instanceof TimeoutException) {
                        return new ResponseEntity<>("서비스 요청 시간이 초과되었습니다. 나중에 다시 시도해 주세요.", HttpStatus.GATEWAY_TIMEOUT);
                    } else if (ex instanceof CallNotPermittedException) {
                        return new ResponseEntity<>("서비스가 현재 사용 불가능합니다. 잠시 후 다시 시도해 주세요.", HttpStatus.SERVICE_UNAVAILABLE);
                    } else {
                        return new ResponseEntity<>("내부 서버 오류가 발생했습니다. 관리자에게 문의해 주세요.", HttpStatus.SERVICE_UNAVAILABLE);
                    }
                })
                .defaultIfEmpty(new ResponseEntity<>("내부 서버 오류가 발생했습니다. 관리자에게 문의해 주세요.", HttpStatus.SERVICE_UNAVAILABLE));
    }

fallback method는 요청 실패에 대한 Exception별로 구분해서 작성할 수 있습니다!

  1. 예외 정보 추출:
    ServerWebExchange에서 CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR 속성을 통해 발생한 예외를 가져옵니다.
  2. 예외 유형별 처리:
    • NotFoundException: 내부 서버 오류 메시지 반환
    • TimeoutException: 서비스 요청 시간 초과 메시지 반환
    • CallNotPermittedException: 서비스 사용 불가 메시지 반환
    • 기타 예외: 일반적인 내부 서버 오류 메시지 반환
  3. 응답 생성:
    예외 유형에 따라 적절한 HTTP 상태 코드(주로 503 또는 504)와 함께 사용자 친화적인 오류 메시지를 ResponseEntity로 감싸 반환합니다.

Resilience4j Property

아래 테이블은 CircuitBreaker/Retry 설정에 사용되는 속성값과 그에 대한 설명입니다.

CircuitBreaker property

propertydescription
failureRateThreshold실패 비율 임계치를 백분율로 설정 해당 값을 넘어갈 시 Circuit Breaker 는 Open 상태로 전환되며, 이때부터 호출을 차단한다 (기본값: 50)
slowCallRateThreshold임계값을 백분율로 설정, CircuitBreaker는 호출에 걸리는 시간이 slowCallDurationThreshold보다 길면 느린 호출로 간주,해당 값을 넘어갈 시 Circuit Breaker 는 Open상태로 전환되며, 이때부터 호출을 차단한다 (기본값: 100)
slowCallDurationThreshold호출에 소요되는 시간이 설정한 임계치보다 길면 느린 호출로 계산.응답시간이 느린 것으로 판단할 기준 시간 (60초, 1000 ms = 1 sec) (기본값: 60000[ms])
permittedNumberOfCallsInHalfOpenStateHALF_OPEN 상태일 때, OPEN/CLOSE 여부를 판단하기 위해 허용할 호출 횟수를 설정 수 (기본값: 10)
maxWaitDurationInHalfOpenStateHALF_OPEN 상태로 있을 수 있는 최대 시간이다. 0일 때 허용 횟수만큼 호출을 모두 완료할 때까지 HALF_OEPN 상태로 무한정 기다린다. (기본값: 0)
slidingWindowTypesliding window 타입을 결정한다. COUNT_BASED인 경우 slidingWindowSize 만큼의 마지막 call들이 기록되고 집계된다.TIME_BASED인 경우 마지막 slidingWindowSize초 동안의 call들이 기록되고 집계됩니다. (기본값: COUNT_BASED)
slidingWindowSizeCLOSED 상태에서 집계되는 슬라이딩 윈도우 크기를 설정한다. (기본값: 100)
minimumNumberOfCallsminimumNumberOfCalls 이상의 요청이 있을 때부터 faiure/slowCall rate를 계산.예를 들어, 해당 값이 10이라면 최소한 호출을 10번을 기록해야 실패 비율을 계산할 수 있다.기록한 호출 횟수가 9번뿐이라면 9번 모두 실패했더라도 circuitbreaker는 열리지 않는다. (기본값: 100)
waitDurationInOpenStateOPEN에서 HALF_OPEN 상태로 전환하기 전 기다리는 시간 (60초, 1000 ms = 1 sec) (기본값: 60000[ms])
recordExceptions실패로 기록할 Exception 리스트 (기본값: empty)
ignoreExceptions실패나 성공으로 기록하지 않을 Exception 리스트 (기본값: empty)
ignoreException기록하지 않을 Exception을 판단하는 Predicate을 설정 (커스터마이징, 기본값: throwable -> true)
recordFailure어떠한 경우에 Failure Count를 증가시킬지 Predicate를 정의해 CircuitBreaker에 대한 Exception Handler를 재정의.true를 return할 경우, failure count를 증가시키게 된다 (기본값: false)

Fallback 처리 테스트


Fallback 처리가 정상적으로 동작하는지, 특정 마이크로서비스를 일부로 다운시킨 후 호출하고 내려오는 응답을 확인해보도록 하겠습니다

🤔 서킷브레이커의 상태변화가 로그로 찍히면 좋겠는데?


Spring Cloud Gateway에서 Resilience4j를 사용하여 서킷 브레이커 패턴을 적용하고, 서킷 브레이커의 상태 변화를 로그로 기록하려면 Resilience4j의 이벤트를 구독하여 상태 변화 시 로그를 남길 수 있습니다!!

1. 서킷 브레이커 상태 변화 이벤트 구독하기

Resilience4j는 서킷 브레이커의 상태 변화나 호출 이벤트를 구독할 수 있는 이벤트 리스너를 제공합니다. 이를 통해 서킷 브레이커의 상태 변화가 발생할 때마다 로그를 기록할 수 있습니다. 아래는 상태 변화와 관련된 이벤트를 구독하여 로그를 남기는 코드 예제입니다.

    @Bean
    public RegistryEventConsumer<CircuitBreaker> circuitBreakerEventConsumer() {
        return new RegistryEventConsumer<CircuitBreaker>() {
            @Override
            public void onEntryAddedEvent(EntryAddedEvent<CircuitBreaker> entryAddedEvent) {
                CircuitBreaker circuitBreaker = entryAddedEvent.getAddedEntry();
                circuitBreaker.getEventPublisher()
                        .onFailureRateExceeded(event -> log.error("Circuit breaker '{}' failure rate {}% exceeded at {}",
                                event.getCircuitBreakerName(), event.getFailureRate(), event.getCreationTime()))
                        .onError(event -> log.error("Circuit breaker '{}' encountered error, duration: {}s",
                                event.getCircuitBreakerName(), event.getElapsedDuration().getSeconds()))
                        .onStateTransition(event -> log.warn("Circuit breaker '{}' state transition from '{}' to '{}' at {}",
                                event.getCircuitBreakerName(), event.getStateTransition().getFromState(),
                                event.getStateTransition().getToState(), event.getCreationTime()));
            }
            @Override
            public void onEntryRemovedEvent(EntryRemovedEvent<CircuitBreaker> entryRemoveEvent) {
                log.debug("Circuit breaker '{}' removed", entryRemoveEvent.getRemovedEntry().getName());
            }

            @Override
            public void onEntryReplacedEvent(EntryReplacedEvent<CircuitBreaker> entryReplacedEvent) {
                log.debug("Circuit breaker '{}' replaced", entryReplacedEvent.getNewEntry().getName());
            }
        };
    }

2. 설명

  • onEntryAddedEvent: 서킷 브레이커가 추가될 때 호출됩니다. 이 이벤트를 통해 서킷 브레이커의 상태 변화, 실패율 초과, 오류 발생 등의 정보를 로그로 남길 수 있습니다.
  • onEntryRemovedEvent: 서킷 브레이커가 제거될 때 호출됩니다. 서킷 브레이커의 제거 정보를 로그로 기록합니다.
  • onEntryReplacedEvent: 서킷 브레이커가 교체될 때 호출됩니다. 교체된 서킷 브레이커의 정보를 로그로 기록합니다.
    아래 사진처럼 서킷 브레이커의 상태 변화가 있을 시, 로그가 남는것을 확인할 수 있습니다!

💻 모니터링

식구 하자 프로젝트에서 장애 전파를 방지하기 위해 Spring Cloud Gateway에 Resilience4j를 이용한 Circuit Breaker 패턴을 적용했습니다. 이 Circuit Breaker의 상태를 actuator를 통해 확인할 수 있지만 매번 일일이 확인하기엔 굉장히 귀찮고 번거로움이 있습니다..

효과적으로 모니터링하고 시각화하기 위해 아래처럼 Grafana를 활용하여 상태를 확인할 수 있습니다

(해당 포스팅에서는 prometheus와 Grafana 적용 과정에 대해 다루지 않습니다!)

🤨 장애? 딱 대

이번 포스팅을 끝으로 Spring Cloud Gateway 패턴에서 장애 대응 하는 방법을 살펴보았습니다. 모든 시스템에는 장애가 발생할 가능성이 있으며, 이를 효과적으로 대응하기 위한 조치는 단순히 개발자가 연동할 시스템에 대한 신뢰 문제나 과도한 설계가 아닙니다. 이는 장애 전파를 방지하기 위한 기본적인 방어 수단입니다.

그렇다면 MSA는 장애가 발생하면 큰일 나는 아키텍처일까요?

버너 보겔스는 "모든 소프트웨어는 결국 실패할 수 있다"고 말했습니다. 이 말에는 중요한 교훈이 숨어 있습니다.

"완벽한 시스템을 만드는 것보다, 실패에 유연하고 빠르게 대응할 수 있는 시스템을 설계하는 것이 중요하다."

이러한 관점에서 볼 때, MSA 아키텍처는 장애에 대한 내성을 높이고, 장애 발생 시 신속하게 대응할 수 있는 시스템을 구축하는 데 적합한 접근 방식입니다. 장애 대응 전략과 도구를 적절히 활용하면, 시스템의 안정성과 신뢰성을 크게 향상시킬 수 있습니다.

이 포스팅이 여러분의 MSA 아키텍처 구축과 장애 대응에 도움이 되길 바랍니다!

오늘도 읽어주셔서 감사합니다 ☺️

참고


https://techblog.woowahan.com/15694/
https://medium.com/@im_zero/spring-cloud-gateway-circuit-breaker-time-limiter-5e3c26a62b4c
https://bkjeon1614.tistory.com/712
https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign-circuitbreaker

profile
백엔드 공부중입니다!

1개의 댓글

comment-user-thumbnail
2024년 8월 15일

좋은 글 감사합니다!

답글 달기