본문 바로가기
해피 코딩/Today I Learned

[TIL 3] CircuitBreaker와 Resilience4j

by happy-coding 2024. 8. 2.

Today I Learned

오늘은 서킷 브레이커에 대하여 공부를 하였다
서비스 디스커버리로드 밸런싱은 어제 수업이 끝나고 추가로 공부하였으므로 주말을 이용하여  기록을 남겨야겠다 🧑‍💻

[ 서킷 브레이커 ]

서킷 브레이커는 마이크로서비스 간의 호출 실패를 감지하고 시스템의 전체적인 안정성을 유지하는 패턴이다. 외부 서비스 호출 실패 시 빠른 실패를 통해 장애를 격리하고, 시스템의 다른 부분에 영향을 주지 않도록 한다.

 

  • 서킷 브레이커의 주요 상태 3가지
    • Closed: 서킷 브레이커의 초기 상태로, 모든 요청이 정상적으로 통과하는 상태를 의미한다.
    • Open: 서킷 브레이커가 설정된 실패 임계치를 초과하여 활성화된 상태이다. 이 상태에서는 모든 요청이 즉시 실패하고, 원래 서비스에 전달되지 않는다.
    • Half - Open: 서킷 브레이커가 일정 시간 동안 Open 상태로 유지된 후, 제한된 수의 요청을 허용하여 시스템의 상태를 테스트하는 상태이다.

CircuitBreaker Closed State
CircuitBreaker Open State

서킷 브레이커 상태 변화 순서: Closed -> Open -> Half - Open

[ Resilience4J ]

Resilience4j는 서킷 브레이커 라이브러리로, 서비스 간의 호출 실패를 감지하고 시스템의 안정성을 유지해 준다. 다양한 서킷 브레이커 기능을 제공하며, 장애 격리 및 빠른 실패를 통해 복원력을 높여준다.
  • Fallback: 호출 실패 시 대체 로직(fallbackMethod)을 사용하여 시스템 안정성 확보하고, 장애가 다른 서비스에 전파되는 것을 방지한다. 장애가 발생해도 사용자에게 일정한 응답을 제공할 수 있다.
Falback 설정 예시
@Service
public class MyService {

  @CircuitBreaker(name = "myService", fallbackMethod = "fallbackMethod")
  public String myMethod() {
    // 외부 서비스 호출
    return externalService.call();
  }

  public String fallbackMethod(Throwable t) {
    return "Fallback response";
  }
}

 


[ Resilience4J 설정 ]

  • 중요❗❗
resilience4j 의존성은 start.spring.io에서 추가하여 사용하지 않고, ”io.github.resilience4j:resilience4j-spring-boot3:2.2.0” 을 사용하였다.
- spring starter에서 추가하여 사용하면 의존성에 구현체가 아닌 인터페이스를 가져온다.

😺 resilience4j User Guide: https://resilience4j.readme.io/docs/getting-started

 

  • buil.grade 파일 예시
    • actuator는 start.spring.io에서 추가해 준다.
dependencies {
	 //actuator는 spring.starter.io에서 추가
	implementation 'org.springframework.boot:spring-boot-starter-actuator'
    	implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
	implementation 'org.springframework.boot:spring-boot-starter-aop'
}

 

  • application.yml 파일 설정
resilience4j:
  circuitbreaker:
    configs:
      default:  # 기본 구성 이름
        registerHealthIndicator: true  # 애플리케이션의 헬스 체크에 서킷 브레이커 상태를 추가하여 모니터링 가능
        # 서킷 브레이커가 동작할 때 사용할 슬라이딩 윈도우의 타입을 설정
        # COUNT_BASED: 마지막 N번의 호출 결과를 기반으로 상태를 결정
        # TIME_BASED: 마지막 N초 동안의 호출 결과를 기반으로 상태를 결정
        slidingWindowType: COUNT_BASED  # 슬라이딩 윈도우의 타입을 호출 수 기반(COUNT_BASED)으로 설정
        # 슬라이딩 윈도우의 크기를 설정
        # COUNT_BASED일 경우: 최근 N번의 호출을 저장
        # TIME_BASED일 경우: 최근 N초 동안의 호출을 저장
        slidingWindowSize: 5  # 슬라이딩 윈도우의 크기를 5번의 호출로 설정
        minimumNumberOfCalls: 5  # 서킷 브레이커가 동작하기 위해 필요한 최소한의 호출 수를 5로 설정
        slowCallRateThreshold: 100  # 느린 호출의 비율이 이 임계값(100%)을 초과하면 서킷 브레이커가 동작
        slowCallDurationThreshold: 60000  # 느린 호출의 기준 시간(밀리초)으로, 60초 이상 걸리면 느린 호출로 간주
        failureRateThreshold: 50  # 실패율이 이 임계값(50%)을 초과하면 서킷 브레이커가 동작
        permittedNumberOfCallsInHalfOpenState: 3  # 서킷 브레이커가 Half-open 상태에서 허용하는 최대 호출 수를 3으로 설정
        # 서킷 브레이커가 Open 상태에서 Half-open 상태로 전환되기 전에 기다리는 시간
        waitDurationInOpenState: 20s  # Open 상태에서 Half-open 상태로 전환되기 전에 대기하는 시간을 20초로 설정

Resilience4j와 Spring Cloud 연동 예시
  • 이번 실습에서는 유레카를 사용하지 않아서 사용법을 기록으로 담겨두었습니다.
  • spring Cloud의 서비스 디스커버리와 로드 밸런싱을 활용하여 더욱 안정적인 마이크로서비스 아키텍처를 구축할 수 있습니다.
spring:
  application:
    name: my-service
  cloud:
    circuitbreaker:
      resilience4j:
        enabled: true

[ 터미널을 통해 서킷 브레이커 상태 보기 ]

  • 서킷 브레이커를 공부하기 위해 ' /product/111 ' 로 요청을 보내면 의도적으로 서킷 브레이커가 발동 되도록 하였다.
정상적인 요청을 통하여 서킷 브레이커 Closed 상태 보기

  • Closed 상태에서 호출이 실패하면 실패 카운터가 증가합니다.
  • 실패율이 설정된 임계값(예: 50%)을 초과하면 서킷 브레이커가 Open 상태로 전환된다.
  • ex) 최근 5번의 호출 중 3번이 실패하여 실패율이 60%에 도달하면 Open 상태로 전환된다.

의도적인 에러를 발생시켜 서킷 브레이커 Open 상태 보기 

에러 발생 시 설정해 둔 EventListenerMethod 가 먼저 동작하고, 이 후 Fallback 되어 fallbackMethod 가 실행된다.

 

실행 Method
// 서킷브레이커 사용 -> 서킷브레이커가 발동되면 EventListener에 먼저 들린 후 fallback이 실행된다
@CircuitBreaker(name = "productService", fallbackMethod = "fallbackGetProductDetails")
public Product getProductDetails(String productId) {
  log.info("###Fetching product details for productId: { }", productId);
  if ("111".equals(productId)) {
    log.warn("###Received empty body for productId: { }", productId);
    throw new RuntimeException("Empty response body");
  }
  return new Product(
          productId,
          "Sample Product"
  );
}

 

 EventListenerMethod

  @PostConstruct
  public void registerEventListener() {
    circuitBreakerRegistry.circuitBreaker("productService").getEventPublisher()
        .onStateTransition(event -> log.info("#######CircuitBreaker State Transition: {}", event)) // 상태 전환 이벤트 리스너
        .onFailureRateExceeded(event -> log.info("#######CircuitBreaker Failure Rate Exceeded: {}", event)) // 실패율 초과 이벤트 리스너
        .onCallNotPermitted(event -> log.info("#######CircuitBreaker Call Not Permitted: {}", event)) // 호출 차단 이벤트 리스너
        .onError(event -> log.info("#######CircuitBreaker Error: {}", event)); // 오류 발생 이벤트 리스너
  }

 

fallbackMethod

public Product fallbackGetProductDetails(String productId, Throwable t) {
    log.error("####Fallback triggered for productId: {} due to: {}", productId, t.getMessage());
    return new Product(
            productId,
            "Fallback Product"
    );
  }

 

 ' /product/111 ' 호출을 통하여 서킷 브레이커가 호출 실패를 감지하고, 설정한 EventListenerMethod 가 먼저 동작하고, 이 후 Fallback 되어 fallbackMethod 가 순서대로 동작하는 모습을 볼 수 있다.

 

  • '/product/111' 여러번 호출 시 Closed -> Open 으로 상태 전환 확인
    • 실패율이 설정된 임계값(예: 50%)을 초과하면 서킷 브레이커가 Open 상태로 전환되면 모든 요청을 즉시 실패로 처리한다.
- 상태 전환 확인을 위해 ' product/111 ' 여러 번 호출
###Fetching product details for productId: 111
###Received empty body for productId: 111

- 오류 발생 이벤트 리스너
#######CircuitBreaker Error: 2024-08-03T13:47:06.739636400+09:00[Asia/Seoul]: CircuitBreaker 'productService' recorded an error: 'java.lang.RuntimeException: Empty response body'. Elapsed time: 0 ms

- 실패율 초과 이벤트 리스너
#######CircuitBreaker Failure Rate Exceeded: 2024-08-03T13:47:06.739636400+09:00[Asia/Seoul]: CircuitBreaker 'productService' exceeded failure rate threshold. Current failure rate: 60.0

- 상태 전환 이벤트 리스너 ( Closed -> Open )
#######CircuitBreaker State Transition: 2024-08-03T13:47:06.746718500+09:00[Asia/Seoul]: CircuitBreaker 'productService' changed state from CLOSED to OPEN

- fallbackMethod
####Fallback triggered for productId: 111 due to: Empty response body

 

  • Open  상태에서는 요청이 실패하지 않고, 바로 에러 응답을 반환된다.
정상적인 요청에 대하여 모든 호출 차단
- 호출 차단 이벤트 리스너
#######CircuitBreaker Call Not Permitted: 2024-08-03T13:47:14.768529700+09:00[Asia/Seoul]: CircuitBreaker 'productService' recorded a call which was not permitted.
- fallbackMethod
####Fallback triggered for productId: 100 due to: CircuitBreaker 'productService' is OPEN and does not permit further calls
...

Closed에서 Open 상태로 전환된 후 2번의 요청에 대하여 EventListener 확인 후 바로 Fallback 되는 모습을 볼 수 있다

  • 서킷브레이커가 Open 상태가 되면 getProductDetails함수를 타지않고 바로 Fallback 되는 것을 확인할 수 있다.
  • ex) 서킷 브레이커가 오픈 상태로 전환되고 20초 동안 모든 요청이 차단된다.
  • 설정된 대기 시간이 지난 후, 서킷 브레이커는 Half - Open 상태로 전환된다.

Open 상태에서 대기 시간이 지나면 서킷 브레이커는 Half - Open 상태로 전환된다
  • Half - Open 상태에서는 제한된 수의 요청을 허용하여 시스템이 정상 상태로 복구되었는지 확인한다.
  • 요청이 성공하면 서킷 브레이커는 Closed 상태로 전환된다.
  • 요청이 실패하면 서킷 브레이커는 다시 Open 상태로 전환된다.
- 상태 전환 이벤트 리스너 ( Open -> Half - Open )
#######CircuitBreaker State Transition: 2024-08-03T13:47:29.338620500+09:00[Asia/Seoul]: CircuitBreaker 'productService' changed state from OPEN to HALF_OPEN

- 정상적인 요청
###Fetching product details for productId: 101
###Fetching product details for productId: 102
###Fetching product details for productId: 103

- 상태 전환 이벤트 리스너 ( Half - Open -> Closed )
#######CircuitBreaker State Transition: 2024-08-03T13:47:37.376962900+09:00[Asia/Seoul]: CircuitBreaker 'productService' changed state from HALF_OPEN to CLOSED
서킷 브레이커 상태 변화 순서: Closed -> Open -> Half - Open

강의를 들으면서 헷갈리던 부분들이 TIL을 작성하면서 정리가 된 것 같다. 기록하는 습관의 중요성을 다시 한번 느끼게 되는 하루인 것 같다.

🔥 오늘 공부의 포인트는 서킷 브레이커가 작동하는 동작 순서를 이해하는 것이 중요한 것 같다.

 

읽어 주셔서 감사합니다 🙂‍↕️

😺 GitHub: https://github.com/mad-cost/Starta-MSA-study-CircuitBreaker