코어모듈은 다음과 같다.
circuitbreaker 와 retry를 사용해 본다.
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
상태 | 설명 |
---|---|
CLOSED | 정상 상태 |
OPEN | 오류 상태 |
HALF_OPEN | OPEN 상태에서 일정 시간 후 장애여부를 판단하여 상태가 변경되기 위해 대기중인 상태 |
옵션 | 기본값 | 설명 |
---|---|---|
failureRateThreshold | 50 | 실패율이 설정값보다 크거나 같으면 서킷브레이커가 OPEN 상태로 전환되고 호출 차단 |
slowCallDurationThreshold | 60000[ms] | 호출시간이 설정값보다 길면 느린호출로 판단 |
slowCallRateThreshold | 100 | 느린호출 비율이 설정값보다 크거나 같으면 서킷브레이커가 OPEN 상태로 전환되고 호출 차단 |
permittedNumberOfCallsInHalfOpenState | 10 | HALF_OPEN 상태일 때 허용되는 호출 수 |
maxWaitDurationInHalfOpenState | 0[ms] | HALF_OPEN 상태에서 대기하는 최대 시간 |
slidingWindowType | COUNT_BASED | 서킷브레이커가 닫힐 때 호출 결과를 기록하는데 사용되는 슬라이딩 윈도우의 유형(COUNT_BASED 또는 TIME_BASED) |
slidingWindowSize | 100 | 슬라이딩 윈도우의 크기 |
minimumNumberOfCalls | 100 | 느린 호출율을 계산하기위해 필요한 최소 호출 수 |
waitDurationInOpenState | 60000[ms] | OPEN -> HALF_OPEN 으로 변경 되기까지 대기 시간 |
automaticTransitionFromOpenToHalfOpenEnabled | false | OPEN -> HALF_OPEN 으로 변경 될 때 자동 여부 |
recordExceptions | empty | 실패로 측정할 예외 리스트 |
ignoreExceptions | empty | 성공/실패 포함 무시할 예외 리스트 |
recordFailurePredicate | throwable -> true | 특정 예외가 실패로 측정되도록 하는 커스텀예외: 기본값으로 모든 예외는 실패로 기록 |
ignoreExceptionPredicate | throwable -> false | 특정 예외가 측정되지 않도록 하는 커스텀예외: : 기본값으로 모든 예외는 무시되지 않는다. |
호출 순번 | 1 | 2 | 3 | 4 | 5 | 6 | ... |
---|---|---|---|---|---|---|---|
정상여부 | success | fail | success | success | fail | fail | ... |
실패율(%) | 0 | 1/2=50 | 1/3=33 | 1/4=25 | 2/5=40 | 3/5=60 | ... |
상태 | CLOSED | CLOSED | CLOSED | CLOSED | CLOSED | OPEN | ... |
시간 흐름(초) | 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 | ... |
상태 | CLOSED | CLOSED | CLOSED | CLOSED | CLOSED | OPEN | ... |
CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults();
CircuitBreaker circuitBreakerWithDefaultConfig = circuitBreakerRegistry.circuitBreaker("test");
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.slowCallRateThreshold(50)
.waitDurationInOpenState(Duration.ofMillis(1000))
.slowCallDurationThreshold(Duration.ofSeconds(2))
.permittedNumberOfCallsInHalfOpenState(3)
.minimumNumberOfCalls(10)
.slidingWindowType(SlidingWindowType.TIME_BASED)
.slidingWindowSize(5)
.recordException(e -> INTERNAL_SERVER_ERROR
.equals(getResponse().getStatus()))
.recordExceptions(IOException.class, TimeoutException.class)
.ignoreExceptions(BusinessException.class, OtherBusinessException.class)
.build();
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(70)
.build();
circuitBreakerRegistry.addConfiguration("someSharedConfig", config);
CircuitBreaker circuitBreaker = circuitBreakerRegistry
.circuitBreaker("name", "someSharedConfig");
@Configuration
public class Resilience4jConfig {
@Bean
public CircuitBreaker circuitBreaker(CircuitBreakerRegistry circuitBreakerRegistry) {
return circuitBreakerRegistry.circuitBreaker(
"customCircuitBreaker",
CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.slowCallRateThreshold(50)
.slowCallDurationThreshold(Duration.ofSeconds(3))
.permittedNumberOfCallsInHalfOpenState(3)
.maxWaitDurationInHalfOpenState(Duration.ofSeconds(3))
.slidingWindowType(COUNT_BASED)
.slidingWindowSize(10)
.minimumNumberOfCalls(5)
.waitDurationInOpenState(Duration.ofSeconds(1))
.build()
);
}
}
resilience4j:
circuitbreaker:
configs:
default:
failureRateThreshold: 50
slowCallRateThreshold: 50
slowCallDurationThreshold: 3000
permittedNumberOfCallsInHalfOpenState: 3
maxWaitDurationInHalfOpenState: 3000
slidingWindowType: COUNT_BASED
slidingWindowSize: 10
minimumNumberOfCalls: 5
waitDurationInOpenState: 1000
instances:
customCircuitBreaker:
baseConfig: default
옵션 | 설명 |
---|---|
name | 등록한 서킷 브레이커의 이름 |
fallbackMethod | 요청이 실패할 경우 실행 메소드 |
@Slf4j
@Service
public class TestService {
@CircuitBreaker(name = "customCircuitBreaker", fallbackMethod = "callFallback")
public String call() throws InterruptedException {
long before = System.currentTimeMillis();
Thread.sleep(5000L);
long after = System.currentTimeMillis();
log.info("[TestService] call => {}ms", after - before);
return "success";
}
private String callFallback(Exception e) {
log.info("[TestService] callFallback");
return "fallback";
}
}
@Test
void circuit_breaker_test() {
for (int i = 0; i < 20; i++) {
testService.call();
}
}
⚙️ 조건은 다음과 같다.
failureRateThreshold: 50
slowCallRateThreshold: 50
slowCallDurationThreshold: 3000
slidingWindowSize: 10
minimumNumberOfCalls: 5
auth-server
@GetMapping("/test/timeout")
public String timeout() throws InterruptedException {
Thread.sleep(5000L);
return "timeout";
}
api-server
⚙️ 조건은 다음과 같다.
failureRateThreshold: 50
slowCallRateThreshold: 50
slowCallDurationThreshold: 3000
slidingWindowSize: 10
minimumNumberOfCalls: 5
@FeignClient(name = "auth-service")
public interface AuthFeignClient {
@CircuitBreaker(name = "customCircuitBreaker", fallbackMethod = "timeoutFallback")
@GetMapping("/test/timeout")
String timeout();
default String timeoutFallback(Throwable e) {
return "[AuthFeignClient] timeoutFallback";
}
}
@Test
void timeout() {
for (int i = 0; i < 20; i++) {
String result = authFeignClient.timeout();
System.out.println("result = " + result);
}
}
기존의 open feign의 retry대신 resilience4j의 retry를 사용한다.
RetryRegistry retryRegistry = RetryRegistry.ofDefaults();
RetryConfig config = RetryConfig.custom()
.maxAttempts(2)
.waitDuration(Duration.ofMillis(1000))
.retryOnResult(response -> response.getStatus() == 500)
.retryOnException(e -> e instanceof WebServiceException)
.retryExceptions(IOException.class, TimeoutException.class)
.ignoreExceptions(BusinessException.class, OtherBusinessException.class)
.failAfterMaxAttempts(true)
.build();
// Retry 생성
RetryRegistry registry = RetryRegistry.of(config);
Retry retryWithDefaultConfig = registry.retry("name1");
RetryConfig custom = RetryConfig.custom()
.waitDuration(Duration.ofMillis(100))
.build();
Retry retryWithCustomConfig = registry.retry("name2", custom);
옵션 | 기본값 | 설명 |
---|---|---|
maxAttempts | 3 | 최대 시도 횟수(첫 번째 호출도 포함) |
waitDuration | 500[ms] | 재시도 사이 대기 시간 |
intervalFunction | numOfAttempts -> waitDuration | 실패 후 대기 간격을 수정하는 기능. 기본적으로 고정 |
intervalBiFunction | (numOfAttempts, Either<throwable, result>) -> waitDuration | 시도 횟수와 결과 또는 예외에 따라 실패 후 대기 간격을 수정. intervalFunction과 함께 사용하면 IllegalStateException이 발생 |
retryOnResultPredicate | result -> false | 결과를 재시도해야 하는지 여부를 평가. true를 반환하면 재시도 |
retryExceptionPredicate | throwable -> true | 예외를 재시도해야 하는지 여부를 평가. true를 반환하면 재시도 |
retryExceptions | empty | 실패로 기록되어 재시도되는 Throwable 클래스 목록. Checked Exceptions를 사용하는 경우 CheckedSupplier를 사용 |
ignoreExceptions | empty | 무시되어 재시도되지 않는 Throwable 클래스 목록 |
failAfterMaxAttempts | false | 재시도가 구성된 maxAttempts에 도달했고 결과가 여전히 retryOnResultPredicate를 전달하지 않는 경우 MaxRetriesExceededException 발생을 활성화 또는 비활성화 |
@Configuration
@RequiredArgsConstructor
public class Resilience4jConfig {
private final RetryRegistry retryRegistry;
@Bean
public Retry retry() {
return retryRegistry.retry(
"customRetry",
RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(300))
.build()
);
}
}
resilience4j:
retry:
configs:
default:
max-attempts: 3
waitDuration: 300
instances:
customRetry:
base-config: default
옵션 | 설명 |
---|---|
name | 등록한 Retry의 이름 |
fallbackMethod | 요청이 실패할 경우 실행 메소드 |
@Slf4j
@Service
public class TestService {
@Retry(name = "customRetry", fallbackMethod = "retryFallback")
public String retry() throws InterruptedException {
log.info("[TestService] retry");
throw new RuntimeException();
}
private String retryFallback(Exception e) {
log.info("[TestService] retryFallback");
return "retryFallback";
}
}
@Test
void retry_test() throws InterruptedException {
testService.retry();
}
⚙️ 조건은 다음과 같다.
max-attempts: 3 // 최대 3번 시도
waitDuration: 1000 // 1초 간격 시도
auth-server
@GetMapping("/test/exception")
public void exception() {
throw new RuntimeException("error");
}
api-server
⚙️ 조건은 다음과 같다.
max-attempts: 3 // 최대 3번 시도
waitDuration: 1000 // 1초 간격 시도
@FeignClient(name = "auth-service")
public interface AuthFeignClient {
@Retry(name = "customRetry", fallbackMethod = "retryFallback")
@GetMapping("/test/exception")
String exception();
default String retryFallback(Exception e) {
return "retryFallback";
}
}
@Test
void exception() {
String result = authFeignClient.exception();
System.out.println("result = " + result);
}
🧨 Resilience4j 의 코어 모듈들은 어노테이션으로 특정 요청에 모두 한번에 적용 가능.
Resilience4j 의 동작 순서는 다음과 같다.
Bulkhead -> TimeLimiter -> RateLimiter -> CircuitBreaker -> Retry
application.yml
resilience4j:
circuitbreaker:
...
circuit-breaker-aspect-order: 1
retry:
...
retry-aspect-order: 2
@Slf4j
@Service
public class TestService {
@Retry(name = "customRetry", fallbackMethod = "retryFallback")
@CircuitBreaker(name = "customCircuitBreaker", fallbackMethod = "circuitBreakerFallback")
public String retryCall() throws InterruptedException {
long before = System.currentTimeMillis();
Thread.sleep(5000L);
long after = System.currentTimeMillis();
log.info("[TestService] retryCall => {}ms", after - before);
throw new RuntimeException();
}
private String retryFallback(Exception e) {
log.info("[TestService] retryFallback");
return "retryFallback";
}
private String circuitBreakerFallback(Exception e) {
log.info("[TestService] circuitBreakerFallback");
return "circuitBreakerFallback";
}
}