
공식문서
참고 포스팅
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'io.github.resilience4j:resilience4j-all'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
CircuitBreaker에서 실패와 지연 횟수/시간 를(을) 집계하기 위해 내부적으로 구현된 알고리즘이 'Sliding Window' 알고리즘 입니다.
고정된 사이즈의 큐 자료구조를 '윈도우'라고 칭합니다.
큐에서 하나의 데이터 주기가 지날때마다, 윈도우 내부 데이터 갯수는 같지만 이동할 앞의 데이터를 추가한 뒤 제일 끝에 있는 데이터를 빼줍니다.
CircuitBreaker를 구현하기 위해서는 방법이 크게 2가지로 구분됩니다.
Java Config Class로 정의하여 사용.
Annotation의 형태로 구현 및 사용.
Annotation 형태로 CircuitBreaker 패턴을 구현하겠습니다.
CircuitBreakerConfig를 통해 관련 설정을 제공합니다. 사용자 정의 전역 CircuitBreakerConfig를 생성하려면 CircuitBreakerConfig Builder를 사용할 수 있습니다.
failureRateThreshold : 실패율 임계치를 백분율로 설정 해당 값을 넘어갈 시 Circuit Breaker 는 Open 상태로 전환되며, 이때부터 호출을 차단합니다 (기본값: 50)
slowCallRateThreshold : 임계값을 백분율로 설정, CircuitBreaker는 호출에 걸리는 시간이 slowCallDurationThreshold보다 길면 느린 호출로 간주,
해당 값을 넘어갈 시 Circuit Breaker 는 Open상태로 전환되며, 이때부터 호출을 차단합니다 (기본값: 100)
slowCallDurationThreshold : 호출에 소요되는 시간이 설정치보다 길면 느린 호출로 계산됩니다. 응답시간이 느린 것으로 판단할 기준 시간 (60초, 1000 ms = 1 sec) (기본값: 60000[ms])
permittedNumberOfCallsInHalfOpenState : HALF_OPEN 상태일 때, OPEN/CLOSE 여부를 판단하기 위해 허용할 호출 횟수 (기본값: 10)
maxWaitDurationInHalfOpenState : HALF_OPEN 상태로 있을 수 있는 최대 시간을 의미합니다. 0일 때 허용 횟수만큼 호출을 모두 완료할 때까지 HALF_OEPN 상태로 무한정 기다립니다. (기본값: 0)
slidingWindowType : sliding window 타입을 결정합니다. COUNT_BASED인 경우 slidingWindowSize 만큼의 마지막 call들이 기록되고 집계됩니다.
TIME_BASED인 경우 마지막 slidingWindowSize초 동안의 call들이 기록되고 집계됩니다. (기본값: COUNT_BASED)
slidingWindowSize : CLOSED 상태에서 집계되는 슬라이딩 윈도우 크기를 설정한다. (기본값: 100)
minimumNumberOfCalls : minimumNumberOfCalls 이상의 요청이 있을 때부터 faiure/slowCall rate를 계산합니다. 예를 들어, 해당 값이 5라면 최소한 호출을 5번을 기록해야 실패 비율을 계산할 수 있습니다. 기록한 호출 횟수가 4번뿐이라면 4번 모두 실패했더라도 circuitbreaker는 열리지 않습니다. (기본값: 100)
waitDurationInOpenState : OPEN에서 HALF_OPEN 상태로 전환하기 전 대기 시간을 말합니다.(60초, 1000 ms = 1 sec) (기본값: 60000[ms])
recordExceptions : 실패로 기록할 Exception 리스트입니다. (기본값: empty)
recordFailure : 어떠한 경우에 Failure Count를 증가시킬지 Predicate를 정의해 CircuitBreaker에 대한 Exception Handler를 재정의.
true를 return할 경우, failure count를 증가시키게 된다 (기본값: false)
ignoreExceptions : 실패나 성공으로 기록하지 않을 Exception 리스트입니다. (기본값: empty)
ignoreException : 기록하지 않을 Exception을 판단하는 Predicate을 설정 (커스터마이징, 기본값: throwable -> true)
Cicuit OPEN, CLOSED, HALF_OPEN 테스트를 위해서 다음과 같이 테스트 코드를 구현하겠습니다.
@RestController
@RequestMapping("test/internal")
public class ErrorfulController {
// 임계값 테스트 - 실패율
@GetMapping("/errorful/case1")
public ResponseEntity<String> case1() {
// Simulate 5% chance of 500 error 랜덤 중 100 중 5가 나올 확률을 시뮬레이팅
int num = new Random().nextInt(100);
if (num < 5) {
System.out.println("num : " + num);
return ResponseEntity.status(500).body("Internal Server Error");
}
return ResponseEntity.ok("Normal response");
}
// 임계값 테스트 - 느린 호출율
@GetMapping("/errorful/case2")
public ResponseEntity<String> case2() {
// Simulate blocking requests every first 10 seconds
LocalTime currentTime = LocalTime.now();
int currentSecond = currentTime.getSecond();
if (currentSecond < 10) {
System.out.println("currentSecond : " + currentSecond);
// Simulate a delay (block) for 10 seconds
try {
System.out.println("===========================================" + System.currentTimeMillis());
Thread.sleep(10000);
System.out.println("===========================================" + System.currentTimeMillis());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return ResponseEntity.status(503).body("Service Unavailable");
}
return ResponseEntity.ok("Normal response");
}
// 슬라이딩 윈도우(시간 기반) - 10초 이내에 오류가 발생하는 경우
@GetMapping("/errorful/case3")
public ResponseEntity<String> case3() {
// Simulate 500 error every first 10 seconds
LocalTime currentTime = LocalTime.now();
int currentSecond = currentTime.getSecond();
if (currentSecond < 10) {
return ResponseEntity.status(500).body("Internal Server Error");
}
return ResponseEntity.ok("Normal response");
}
}
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
# CircuitBreaker 설정
resilience4j.circuitbreaker:
instances:
cb-case:
base-config: default
configs:
default:
# sliding window
sliding-window-type: count_based # Sliding Window 타입 : 횟수 or 시간
sliding-window-size: 10 # Sliding Window Size : 총 10개를 기준으로 집계 수행
# fail
failure-rate-threshold: 20 # 임계치 : sliding window size 가 10인 경우, 10개 중 1갬만 fail 이 발생해도, 임계치 이상 fail 발생으로 인해 CircuitBreaker 가 open 됩니다.
minimum-number-of-calls: 5 # 10개를 굳이 채우지 않더라도, 최소 5회 요청 집계 후 임계치 이상일 경우, CircuitBreaker 를 Open 해주는 설정.
# delay
slow-call-rate-threshold: 10 # Circuit Open 지연 비율 설정, 지연수 / 슬라이딩 윈도우 크기, 10% 임계치 설정
slow-call-duration-threshold: 3000ms #3초 이상 지연 시, 지연 요청
# half open
permitted-number-of-calls-in-half-open-state: 10 # open 된 상태에서 최소 10회 요청을 수행, 성공 시 close 실패 시 open
# half open 유지 시간
max-wait-duration-in-half-open-state: 0 # 0인 경우, 설정한 값만큼 수행 후 바로 다음 상태로 전환
wait-duration-in-open-state: 6000ms # open 상태 유지 시간, 해당 시간 지연 후 half-open 으로 전환
automatic-transition-from-open-to-half-open-enabled: true # half open 전환 시 자동으로 전환할 지 여부를 체크
# actuator 설정
register-health-indicator: true # open / close 여부를 체크
# 예외 처리 설정 - 예상되는 예외만 실패처리에서 제외
# record-exceptions:
# - java.io.IOException
# - java.util.concurrent.TimeoutException
# - org.springframework.web.client.HttpServerErrorException
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("test/internal/errorful/case1")
String case1();
@GetMapping("test/internal/errorful/case2")
String case2();
@GetMapping("test/internal/errorful/case3")
String case3();
}
@RestController
@RequestMapping("test")
@RequiredArgsConstructor
public class ErrorController {
private final ErrorService errorService;
@GetMapping("case1")
public ResponseEntity<?> test1(){
String result = errorService.errorTest1();
return ResponseEntity.ok(result);
}
@GetMapping("case2")
public ResponseEntity<?> test2(){
String result = errorService.errorTest2();
return ResponseEntity.ok(result);
}
@GetMapping("case3")
public ResponseEntity<?> test3(){
String result = errorService.errorTest3();
return ResponseEntity.ok(result);
}
}
@Slf4j
@Service
@RequiredArgsConstructor
public class ErrorService {
private final UserClient userClient;
@CircuitBreaker(name = "cb-case", fallbackMethod = "fallbackMethod1")
public String errorTest1(){
return userClient.case1();
}
public String errorTest2(){
return userClient.case2();
}
public String errorTest3(){
return userClient.case3();
}
private String fallbackMethod1(Throwable throwable){
return "[fallback] : " + throwable.getMessage();
}
}
초기 테스트 전

실패율 0% (0/ 10)

실패율 10% (1/ 10)

1회 실패가 발생했지만, 임계치가 20%이므로, Circuit이 CLOSED 상태입니다.
💡 참고
슬라이딩 윈도우로 동작하기 때문에, 1회 실패한 뒤 10회의 성공이 발생할 경우, 실패횟수는 다시 0이 됩니다.
실패율 20% - OPEN

임계치(20%) 도달 시, 정상적으로 Circuit이 OPEN 되는것을 확인할 수 있습니다.
HALF_OPEN
아래 설정으로 인해 6초 뒤 OPEN → HALF_OPEN으로 전환됩니다.
wait-duration-in-open-state: 6000ms # open 상태 유지 시간, 해당 시간 지연 후 half-open 으로 전환

최소 수행횟수 10회 이지만, minimum-number-of-calls 설정으로 인해 5회 기준으로 집계를 하여 임계치 미만 시 CLOSED, 이상 시 OPEN으로 전환됩니다.
minimum-number-of-calls: 5 # 10개를 굳이 채우지 않더라도, 최소 5회 요청 집계 후 임계치 이상일 경우, CircuitBreaker 를 Open 해주는 설정.
Fallback 함수
설정
@Slf4j
@Service
@RequiredArgsConstructor
public class ErrorService {
private final UserClient userClient;
@CircuitBreaker(name = "cb-case", fallbackMethod = "fallbackMethod1")
public String errorTest1(){
return userClient.case1();
}
public String errorTest2(){
return userClient.case2();
}
public String errorTest3(){
return userClient.case3();
}
private String fallbackMethod1(Throwable throwable){
return "[fallback] : " + throwable.getMessage();
}
}
위처럼 fallback 함수명을 fallbbackMethod에 설정 추가해주면, 실패 시마다 해당 fallback 함수가 수행됩니다.


현재 시간의 초가 10초 이내인 경우, 10초 동안의 시간 지연 발생하도록 sleep 메서드를 수행합니다.
현재 지연 초과 시간 설정은 3초입니다.
slow-call-duration-threshold: 3000ms
3초 이상 지연 발생 시, 다음과 같이 실패 및 지연 횟수가 올라가게 됩니다.

지연율 임계치가 10% 이므로, Circuit이 정상적으로 OPEN 됩니다.

기존의 Count_Base와는 다르게 Time_Base의 경우, 10의 의미는 10초 동안에 실패율 20% 이상을 달성했는지 여부를 확인하며, 위의 경우 10초 내 발생했던 요청들이 모두 실패하여 OPEN 된 상태입니다.
재시도와 관련된 Resilience4j 라이브러리입니다.
실패한 실행을 짧은 지연을 가진 후 재시도하게 됩니다.
retry의 기본적인 우선 순위는 CircuitBreaker 보다 낮기 때문에, CircuitBreaker 실행 후 실행하게 됩니다. 이로 인해, fallbackmethod를 등록한 circuit의 경우 처리를 하면, 안됩니다.
만약, fallbackmethod 동작 전 retry 를 수행하고 싶을 경우, Config 설정에서 우선순위를 변경해주면 됩니다.
RetryConfig를 통해 관련 설정을 제공합니다. 사용자 정의 전역 RetryConfig를 생성하려면 RetryConfig Builder를 사용할 수 있습니다.
maxAttempts : 최대 시도 횟수(처음 요청 시도를 포함한 횟수입니다.)
waitDuration : 재시도 간에 지연 시간(ms)
intervalFunction : 동적으로 waitDuration을 만들고자 할 때 사용
intervalBiFunction : 동적으로 waitDuration을 만들고자 할 때 사용(매개변수 2개)
retryOnResultPredicate : 반환 결과에 따라서 retry 여부를 결정하는 필터(true - retry, false - ignore)
retryExceptionPredicate : 해당 exception 종류에 따라서 retry 여부를 결정하는 필터
retryExceptions : 실패로 기록하고 재시도하는 Exception 리스트.
ignoreExceptions : Exception이 발생해도 ignore하는 Exception 리스트.
failAfterMaxRetries : 모든 시도를 실패했을 때 MaxRetriesExceededException를 리턴할지 아니면 해당 Exception을 리턴할지 정하는 값
# Retry 설정
resilience4j.retry:
instances:
rt-case:
base-config: default
configs:
default:
max-attempts: 4
wait-duration: 2000ms
@Slf4j
@Service
@RequiredArgsConstructor
public class ErrorService {
private final UserClient userClient;
// @CircuitBreaker(name = "cb-case", fallbackMethod = "fallbackMethod1")
@Retry(name = "rt-case", fallbackMethod = "fallbackRetry")
public String errorTest1(){
return userClient.case1();
}
...
private String fallbackRetry(Exception e){
return "[retry-fallback] : " + e.getMessage();
}
}
요청 중 실패 발생할 경우, 해당 요청에 대해 Retry가 실행됩니다.(2초마다 4번)

Retry 중 한번이라도 요청에 성공할 경우, 정상 응답을 반환합니다.

만약, Retry를 했음에도 요청을 실패한 경우, fallback 함수에 정의된 함수가 호출됩니다.
