CircuitBreaker
Fault Tolerance(=장애 허용 시스템) 에서 사용되는 대표적인 패턴으로 서비스에서 타 서비스 호출 시 에러, 응답지연, 무응답, 일시적인 네트워크 문제 등을 요청이 무작위로 실패하는 경우에 Circuit를 오픈하여 장애가 다른 서비스로 전파되지 못하도록 방지하고 미리 정의해놓은 Fallback Response를 보내어 서비스 장애가 전파되지 않도록 하는 패턴이다. CircuitBreaker를 사용하는 목적은 애플리케이션의 안정성과 장애 저항력을 높이는 데 있다.
- 상태 정상
- Client -> Service A -> Circuit Breaker (정상 상태: Bypass Traffic) -> Service B
- 장애상황
- Client -> Service A <-> Circuit Breaker (상태: 장애상황이므로 Fallback Message 처리) | Service B (Circuit Breaker 에 의해서 Service B 까지 요청이 도달하지 않음)
상태 | 설명 |
CLOSED | 초기 상태로 모든 접속이 일반적으로 실행 |
OPEN | 에러율이 임계치를 넘어가면 OPEN 상태로 변경, 모든 접속 차단(Fail Fast) 실제 요청을 실행하지 않고 바로 에러 및 디폴트 응답으로 처리 |
HALF_OPEN | OPEN 상태 중간에 한번씩 요청을 날려 응답이 성공인하는지 체크하는 상태 OPEN 후 일정 시간이 지나면 HALF_OPEN 상태로 변경 됨 중간중간 접속 체크에서 성공하면 CLOSE, 실패하면 OPEN으로 변경처리 함 |
Java 기반 CircuitBreaker
1. Netflix Hystrix
- Netflix OSS 제품군, Spring과 Integration해서 Spring Cloud Netflix 프로젝트를 제공하였음
- 현재 지원종료 상태(Spring Boot 2.4.X 부터는 더 이상 지원하지 않음), Spring에서는 아래 내용을 권장
2. Resilience4j
- 사용하기 가볍고, 다른 라이브러리 의존성 없음
- Java 전용으로 개발된 경량화된 Fault Tolerance Libray
- Spring에서 Hystrix 대안으로 권장하는 라이브러리
Resilience4j
resilience4j는 CircuitBreaker 외에 아래와 같이 다양한 기능들을 제공하고 각 기능들에 대해 모듈 라이브러리를 제공하고 있다.
모듈 | 설명 |
resilience4j-circuitbreaker | CircuitBreaker 처리 |
resilience4j-bulkhead | 병렬 작업 제한 관리 |
resilience4j-ratelimiter | 요청 제한 관리 |
resilience4j-retry | 재시도 관리 |
resilience4j-timelimiter | 실행 시간 제한 관리 |
resilience4j-cache | 캐시 처리 |
Resilience4j CircuitBreaker
필자가 관심있는 부분은 CircuitBreaker임으로 먼저 CircuitBreaker 부터 알아보고 나중에 다른 모듈들을 알아가보자.
CircuitBreaker에서 설정할 수 있는 옵션은 대략 아래와 같다. 디폴트값들이 있어서 필요한 부분만 설정하면 된다.
옵션 | 디폴트 값 | 설명 |
failureRateThreshold | 50 % | Circuit이 OPEN되는 실패율 |
slowCallDurationThreshold | 60000 ms | 느린 호출(slow call)로 판단하는 호출 시간 |
slowCallRateThreshold | 100 | Circuit이 OPEN되는 느린호출 비율(slow call rate) |
permittedNumberOfCallsInHalfOpenState | 10 | HALF_OPEN 상태일 때 허용되는 호출 수 |
maxWaitDurationInHalfOpenState | 0 ms | HALF_OPEN 상태에서 대기하는 최대 시간 |
slidingWindowType | COUNT_BASED | CircuitBreaker 호출 결과를 기록하는데 사용되는 슬라이딩 윈도우 타입 (COUNT_BASED or TIME_BASED) |
slidingWindowSize | 100 | 슬라이딩 윈도우 크기 COUNT_BASED = array 크기 TIME_BASED = 초 |
minimumNumberOfCalls | 100 | Circuit 동작시키기위한 최소한의 call 수 (실패 or 느린 호출을 계산하기 위해 필요한 최소한의 호출 수) |
waitDurationInOpenState | 60000 ms | OPEN -> HALF_OPEN으로 변경되는 대기 시간 |
automaticTransitionFromOpenToHalfOpenEnabled | false | OPEN -> HALF_OPEN 자동 변경 여부 |
recordExceptions | empty | 실패로 측정하는 예외 리스트 (명시하면 명시하지 않은 예외는 성공으로 간주) |
ignoreExceptions | empty | 실패로 처리하지 않을예외 리스트 |
recordFailurePredicate | throwable -> true | 특정 예외가 실패로 측정되도록 하는 커스텀예외 (기본값으로 모든 예외는 실패로 기록) |
ignoreExceptionPredicate | throwable -> false | 특정 예외가 측정되지 않도록 하는 커스텀예외 (기본값으로 모든 예외는 무시되지 않음) |
- COUNT_BASED 슬라이딩 윈도우 타입: 호출 횟수를 기반으로 슬라이딩 윈도우를 잡는 방법. N 크기의 circular array를 생성해서 해당 array의 실패률을 이용하여 circuit의 상태를 확정.
- TIME_BASED 슬라이딩 윈도우 타입: 시간을 기반으로 슬라이딩 윈도우를 잡는 방법. N 초의 슬라이딩 윈도우 크기라면 N 개의 circular array를 생성하고 버킷에 특정 초에 발생한 호출의 결과를 집계 및 저장하고 있고, 이를 초가 지남에 따라 밀어내는 방식.
아래와 같이 옵션을 설정했다고 했을 때 슬라이딩 윈도우 타입 별로 동작 샘플을 알아보자
- slidingWindowSize = 5
- minimumNumberOfCalls = 3
- failureRateThreshold = 50(%)
- slowCallDurationThreshold = 3000(ms)
1. COUNT_BASED
호출 순서 | 1 | 2 | 3 | 4 | 5 | 6 |
성공/실패 | 성공 | 실패 | 성공 | 성공 | 실패 | 실패 |
실패율(%) | 0 | 1/2=50% | 1/3=33% | 1/4=25% | 2/5=40% | 3/5=60% |
Circuit 상태 | CLOSED | CLOSED | CLOSED | CLOSED | CLOSED | OPEN |
- 실패 케이스: 예외 발생인 경우와 느린 호출(3000ms 이상) 인 경우
- 2번째 호출에서 실패율이 50%가 되었지만 Circuit을 동작시키기위한 최소한의 call 수(minimumNumberOfCalls)가 3이 아니기 때문에 계속 CLOSED 상태를 유지
- 슬라이딩 윈도우 크기(slidingWindowSize)가 5임으로 6번째 호출이 들어오면 호출순서 2~6(5개)번째 호출을 기준으로 계산
2. TIME_BASED
호출 순서(초) | 1 | 2 | 3 | 4 | 5 | 6 |
호출 횟수 | 1 | 0 | 3 | 2 | 4 | 2 |
실패 횟수 | 0 | 0 | 1 | 1 | 2 | 2 |
실패율(%) | - | - | 1/4=25% | 2/6=33% | 4/10=40% | 6/11=55% |
Circuit 상태 | CLOSED | CLOSED | CLOSED | CLOSED | CLOSED | OPEN |
- 실패 케이스: 예외 발생인 경우와 느린 호출(3000ms 이상) 인 경우
- 매 초마다 전체 호출 횟수 대비 실패 횟수로 실패율을 계산
- 3초(minimumNumberOfCalls) 이후 부터 5초(slidingWindowSize) 이내의 실패율로 계산해서 6초에서는 2~6초의 실패율 계산 결과 55%가 되어 Cicuit OPEN 됨
SpringBoot Resilience4j 적용
먼저 아래와 같이 의존성 설정을 한다. 필자는 SpringBoot3를 사용하고 있어서 resilience4j-spring-boot3를 설정했고, 추가적으로 aop, actuator를 선언해준다.
implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.1.0'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-aop'
application.yml 설정을 아래와 같이 해준다. 위에서 설명한 옵션을 설정하는 부분이다.
resilience4j:
circuitbreaker:
configs:
default: # 기본 config 명
registerHealthIndicator: true
# COUNT_BASED(마지막 N번의 호출 결과를 기반), TIME_BASED(마지막 N초의 결과를 기반) (호출 결과를 저장하고 집계하기 위해 slidingWindow 사용)
slidingWindowType: COUNT_BASED
# COUNT_BASED: array크기, TIME_BASED: 초
slidingWindowSize: 5
# circuit을 동작시키기위한 최소한의 call 수
minimumNumberOfCalls: 5
slowCallRateThreshold: 100
slowCallDurationThreshold: 60000
failureRateThreshold: 50 # 실패율 임계값(50%)
permittedNumberOfCallsInHalfOpenState: 3
# 서킷의 상태가 Open 에서 Half-open 으로 변경되기전에 Circuit Break가 기다리는 시간[s](HALF_OPEN 상태로 빨리 전환되어 장애가 복구 될 수 있도록)
waitDurationInOpenState: 20s
instances:
circuit-sample-common: # circuitbreaker name
baseConfig: default # 기본 config 지정
circuit-sample-3000:
baseConfig: default
slowCallDurationThreshold: 3000 # 응답시간이 느린것으로 판단할 기준 시간 [ms]
이제 CircuitBreaker를 적용하는 Service 클래스를 작성해준다.
@Service
@Slf4j
public class CircuitBreakerService {
/**
* 응답 Delay 샘플
* : application.yml 설정의 'slidingWindowSize', 'minimumNumberOfCalls', 'failureRateThreshold' 따라 6번째 호출부터 fallbackMethod 실행
*/
@CircuitBreaker(name = "circuit-sample-3000", fallbackMethod = "fallback")
public String slowCall(){
long start = System.currentTimeMillis();
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
log.info("[slowCall] call => {}ms", end - start);
return "success";
}
/**
* Exception 응답 샘플
*/
@CircuitBreaker(name = "circuit-sample-common", fallbackMethod = "fallback")
public String exceptionCall() {
log.info("[exceptionCall] call!!");
int i = 1/0; // Exception 발생 시 즉시 fallbackMethod 실행 됨
log.info("i {}", i);
return "exception";
}
/**
* CircuitBreaker Fallback 메소드
*/
private String fallback(Throwable t) {
log.info("[fallbackMethod] call!!");
return "fallback";
}
}
다음으로 테스트용 Controller를 작성해본다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/circuitbreaker")
public class CircuitBreakerController {
private final CircuitBreakerService circuitBreakerService;
@GetMapping(value = "/slowcall")
public String slowCall(){
return circuitBreakerService.slowCall();
}
@GetMapping(value = "/exceptionCall")
public String exceptionCall(){
return circuitBreakerService.exceptionCall();
}
}
설정 값들을 변경해가면서 테스트를 진행해보면 동작 방식을 이해하는데 도움이 될 것 같다.
참고:
https://velog.io/@haerong22/Spring-Cloud-를-이용한-MSA-4.-장애처리Resilience4j
https://mein-figur.tistory.com/entry/resilience4j-circuit-breaker-example
'개발 > SpringBoot' 카테고리의 다른 글
SpringBoot Redis Cache 적용 - @Cacheable, @CacheEvict (0) | 2024.10.14 |
---|---|
Invalid value type for attribute 'factoryBeanObjectType': java.lang.String 에러 해결 (1) | 2024.10.09 |
SpringBoot AWS S3 한글명 파일업로드 에러 (0) | 2023.08.20 |
Gradle build jar 시 plain(xxxx-plain.jar) 제거하기 (0) | 2023.07.22 |
springdoc swagger ui disable Petstore(swagger-ui enabled false not working) (0) | 2023.06.19 |
댓글