https://kimyhcj.tistory.com/entry/Spring-Cloud-%EC%8B%9C%EB%A6%AC%EC%A6%88-3-Circuit-Breaker-Hystrix
앞선 포스팅에서 Netflix Hystrix 에 대해서 알아 봤다.
이번 시간엔 Spring Boot 에 기본적으로 포함되어 있는 Circuit Breaker 인 Resilience4j 에 대해서 알아보자.
Hystrix 는 현재 새로운 release version 이 나오지 않고 있으며 공식적으로 추가 개발은 없고 유지보수 상태라고 말하고 있다.
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% 로 정하면 어떻게 될까?
'IT > Spring Cloud' 카테고리의 다른 글
Spring Cloud 시리즈 4 - Service Discovery (feat. Eureka) (0) | 2023.05.30 |
---|---|
Spring Cloud 시리즈 3 - Resilience4j #2 (0) | 2023.05.25 |
Spring Cloud 시리즈 3 - Circuit Breaker (Hystrix) (0) | 2023.05.22 |
Error creating bean with name 'configurationPropertiesBeans' defined in org.springframework.cloud.autoconfigure.... (0) | 2023.05.22 |
Spring Cloud 시리즈 1 - 개요 (0) | 2023.05.17 |