본문 바로가기
IT/Spring Cloud

Spring Cloud 시리즈 3 - Resilience4j #1

by 최고영회 2023. 5. 23.
728x90
반응형
SMALL

https://kimyhcj.tistory.com/entry/Spring-Cloud-%EC%8B%9C%EB%A6%AC%EC%A6%88-3-Circuit-Breaker-Hystrix

 

Spring Cloud 시리즈 3 - Circuit Breaker (Hystrix)

Circuit Breaker Pattern Software system 이 네트워크상 서로 다른 소프트웨어를 원격으로 호출하는 것은 매우 일반적이다. 특히 Cloud 환경에서 MSA 로 서비스를 구현하는 경우라면 더욱 그렇다. 메모리 내

kimyhcj.tistory.com

 

앞선 포스팅에서 Netflix Hystrix 에 대해서 알아 봤다.

 

이번 시간엔 Spring Boot 에 기본적으로 포함되어 있는 Circuit Breaker 인 Resilience4j 에 대해서 알아보자. 

 

Hystrix 는 현재 새로운 release version 이 나오지 않고 있으며 공식적으로 추가 개발은 없고 유지보수 상태라고 말하고 있다.

Hystrix is now officially in maintenance mode

 

Spring Cloud 에서도 이와 관련하여 대체할 수 있는 모듈들을 제공하고 있다. 

  • Hystrix => Resilience4j
  • Hystrix Dashboard / Turbine => Micrometer + Monitoring System
  • Ribbon => Spring Cloud Loadbalancer
  • Zuul 1 => Spring Cloud Gateway
  • Archaius 1 => Spring Boot external config + Spring Cloud Config

Resilience4j

Netflix Hystrix 에서 영감을 받아 만든 장애를 견디게 해 주는 경량 라이브러리이다. 

Hystrix 는 Java 6 이 기반이었으며 Resilience4j 는 Java 8 기반이라 함수형 기반으로 개발되었다는 차이가 있다. 

또한 Hystrix 는 내부적으로 Guava, Apache Common 과 같은 덩치가 큰 모듈들을 의존하고 있는 반면

Resilience4j는 함수형 프로그래밍을 위한 모듈은 vavr 만을 의존하고 있어 더 가볍다. 

 

Resilence4j 가 장애를 처리하는 대표적인 패턴은 Hystrix 와 크게 다르지 않으며 이름도 동일하다. 

  • Retry: 실패한 실행을 짧은 지연을 가진 후 재시도 
  • Circuit Breaker: 실패한 실행에 대해서 또 다른 시도가 들어올 때 바로 실패 처리 
  • Rate Limiter: 일정 시간동안 들어올 수 있는 요청을 제한 
  • Time Limiter: 실행 되는 시간을 특정 시간을 넘지 않도록 제한 
  • Bulkhead: 동시에 실행할 수를 제한 
  • Cache: 성공적인 실행을 캐싱
  • Fallback: 실행을 실패하는 경우 대신 실행하게 하는 프로세스

 

Retry

official doc: https://resilience4j.readme.io/docs/retry

 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
 <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-retry</artifactId>
</dependency>

필요한 dependency 추가

 

server:
  port: 8081
resilience4j:
  retry:
    instances:
      getProductPrice:
        maxRetryAttempts: 3
        waitDuration: 1s

application.yaml 에 resilience4j 설정 추가 (최대 3번 1초 간격으로 재시도하도록 설정)

instances > "getProductPrice" 는 실제 코드에 @Retry 적용시 설정하는 name

 

 

@Service @RequiredArgsConstructor @Slf4j
public class TestService {
    final RestTemplate rest;
    final RetryRegistry retryRegistry;

    @PostConstruct
    public void init() {
        retryRegistry.getAllRetries().forEach(retry -> retry.getEventPublisher().onRetry(ev -> log.warn("{}", ev)));
    }

    @Retry(name = "getProductPrice", fallbackMethod = "failedGetProductPrice")
    public String getProductPrice(String productId) {
        String rtn = rest.getForObject("http://localhost:8080/product/"+productId, String.class);
        log.info("Success get product price");
        return rtn;
    }
    public String failedGetProductPrice(String productId, Throwable t) {
        log.error("Failed get product price");
        return "sold out";
    }
}

@PostConstruct 부분은 재시도하는 것을 로깅해 보기 위해 적용한 코드이기에 생략 가능합니다. 

 

 

External API 에서는 random 하게 throw RuntimeException 하도록 해 둔 상태에서 테스트 해 보면

최대 3번까지 재시도 하는 것을 확인할 수 있고 

3번째 재시도까지 실패하게 되면 fallback method 에서 처리되는 것을 확인할 수 있다.

 

 

Circuit Breaker

 

official doc: https://resilience4j.readme.io/docs/circuitbreaker

필요한 dependency 는 위에서 Retry 시 이미 추가해 두었고

server:
  port: 8081
resilience4j:
  retry:
    configs:
      default:
        maxRetryAttempts: 5
        waitDuration: 2s
    instances:
      getProductPrice:
        baseConfig: default
        maxRetryAttempts: 3
        waitDuration: 1s
    retry-aspect-order: 2
  circuitbreaker:
    configs:
      default:
        registerHealthIndicator: true
        automaticTransitionFromOpenToHalfOpenEnabled: true # open -> half 자동변환 on/off
        slidingWindowType: TIME_BASED # TIME_BASED, COUNT_BASED
        slidingWindowsSize: 100 # 통계 큐 사이즈
        permittedNumberOfCallsHalfOpenState: 10 # half 내 통계 큐 사이즈
        waitDurationInOpenState: 10000  # circuit open 시간 (default 60_000)
        minimumNumberOfCalls: 3 # 최소한 n번은 호출해야 실패 비율을 계산하겠다.
        slowCallRateThreshold: 100 # 느린 응답시간 요청의 비율이 n% 이상 되면 circuit open
        slowCallDurationThreshold: 500 # 느린 응답시간 판단 기준 [ms] - hystrix 의 timeout 값과 같음
        failureRateThreshold: 50 # 실패한 호출에 대한 임계치(percentage), 이 값을 초과하면 circuit open
    instances:
      getProductPrice:
        baseConfig: default
    circuit-breaker-aspect-order: 1

.yaml 에 필요한 설정 정보들을 set 해준다. 

최소 3번의 요청 이후 500ms 이상 응답이 느릴 경우 circuit 을 10초간 open 하도록 설정했다.

 

 

@Service @RequiredArgsConstructor @Slf4j
public class TestService {
    final RestTemplate rest;
    final RetryRegistry retryRegistry;
    final CircuitBreakerRegistry circuitRegistry;

    @PostConstruct
    public void init() {
        retryRegistry.getAllRetries().forEach(retry -> retry.getEventPublisher().onRetry(ev -> log.warn("{}", ev)));
        circuitRegistry.getAllCircuitBreakers().forEach(breaker -> breaker.getEventPublisher().onEvent(ev -> log.warn("{}", ev)));
    }

    @CircuitBreaker(name = "getProductPrice", fallbackMethod = "failedGetProductPriceCb")
    public String getProductPrice(String productId) {
        String rtn = rest.getForObject("http://localhost:8080/product/" + productId, String.class);
        log.info("Success get product price");
        return rtn;
    }
    public String failedGetProductPriceCb(String productId, Throwable t) {
        log.error("[CircuitBreaker] Failed get product price");
        return "sold out";
    }
}

@CircuitBreaker 를 이용해 쉽게 설정할 수 있다. 

Hystrix 는 .yaml 로 글로벌 설정하거나 annotation 에 hystrix property 로 개별 설정한 반면

resiliency4j 는 .yaml 에 instance 별로 설정하거나 별도의 @configuration 으로 cofnig 설정이 가능하다. 

hystrix 처럼 annotation 으로 개별 설정은 불가능하다. 

별도 configuration 을 통해 설정이 가능하고 instance 별로 .yaml 에서 설정 가능하기 때문에 이 방법도 괜찮은 것 같다. 

 

External API 에서 Thread.sleep(1000) 해서 응답을 느리게 하고 테스트 해 보면

3번까지 Success get product price 되고 CircuitBreaker 가 Elapsed time 을 recording 하고 있다. 

3번까지의 통계 기록이 모두 thresold 기준값인 100% (설정) 에 만족하기 때문에

Circuit 의 상태가 CLOSED 에서 OPEN 되었고 이후 요청에 대해서는 설정한 시간만큼 fallback method 를 통해 처리되는 것을 볼 수 있다. 

이후 waitDurationInOpenState 시간이 지나 Circuit 의 상태가 OPEN 에서 HALF_OPEN 으로 바뀌고

다시 요청에 대한 처리 시간을 recording 하며 반복된다.

automaticTransitionFromOpenToHalfOpenEnabled 설정이 false 일 경우 waitDurationInOpenState 시간이 지나더라도 다음 요청이 들어올 때 까지 circuit 의 상태는 변경되지 않는다. 

 

 

만약 Retry 와 CircuitBreaker 를 동시에 설정하면 어떻게 될까?

@CircuitBreaker(name = "getProductPrice", fallbackMethod = "failedGetProductPriceCb")
@Retry(name = "getProductPrice", fallbackMethod = "failedGetProductPriceRt")
public String getProductPrice(String productId) {
    String rtn = rest.getForObject("http://localhost:8080/product/" + productId, String.class);
    log.info("Success get product price");
    return rtn;
}

 

 External API 에서는 random 하게 RuntimeException 을 throw 하도록 하고 sleep 은 제거한 상태에서 테스트 한다.

CircuitBreaker 설정은 3회 이상 집계하여 실패율이 50%가 되면 circuit 이 open 되도록 설정했다.

 - failureRateThreshold: 50

 - minimumNumberOfCallls: 3

테스트를 해 보면 CircuitBreaker 는 의도한 대로 정상 동작하는 것을 볼 수 있는데

실패 했을 때 Retry 를 전혀 시도하고 있지 않은 것을 볼 수 있다. 

resilience4j 의 aspect order 를 살펴 보면

반응형

Retry( CircuitBreaker( RateLimiter( TimeLimiter( Bulkhead( function))))) 이기 때문에 

CircuitBreaker 가 우선순위가 높아 CircuitBreaker 에 걸려 Retry 가 동작하지 않게 되는 것이다. 

즉, 실패가 되었을때 circuitbreaker 의 fallback method 에서 예외가 처리되기 때문에 원래 method 는 throw 가 발생하지 않게 되고 이로인해 retry 의 fallback 이 감지하지 못하게 되는 것이다. 

retry:
  retry-aspect-order: 2
  
circuitbreaker:
  circuit-breaker-aspect-order: 1

이렇게 .yaml 에 우선순위를 정하면 실패할 경우 retry 를 먼저 수행하게 된다. 

 

 

Quiz

만약 extern api 에서 100% throw new RuntimeException(""); 하고

client 에서는 retry 최대값을 5로 설정 하고

circuitbreaker 의  minimumNumberOfCallls 를 3으로 하고 rate 를 50% 로 정하면 어떻게 될까?

 

728x90
반응형
LIST