1. 개요
https://resilience4j.readme.io/docs/getting-started
Hystrix에서 영감을 얻어 만든 라이브러리로 Resilience4j입니다.
가볍고 다른 라이브러리에 의존성이 없고
Circuit Breaker, Rate Limier, Time Limiter, Bulkhead, Retry, Cache 구현체가 있습니다.
2. 설정 설명
아래 사이트를 참고합니다.
https://resilience4j.readme.io/docs/circuitbreaker
1. CircuitBreaker Config
요청 실패율이 특정 값 이상일 경우 서킷의 상태를 open/close 여부를 결정하여 에러에 대한 대응을 할 수 있습니다.
Config | propertyDefault | ValueDescription |
failureRateThreshold | 50 | 실패율(failure ratio) threshold % |
slowCallRateThreshold | 100 | 지연된 응답 threshold % |
slowCallDurationThreshold | 60000 [ms] | 요청이 느린 것으로 간주하는 ms 초 |
permittedNumberOfCalls InHalfOpenState |
10 | Half-open 상태에서 허가되는 요청 수 |
maxWaitDurationInHalfOpenState | 0 | Half-open 상태에서 대기할 수 있는 최대 시간으로 모든 허가된 요청이 완료될 때까지 0은 무기한으로 기다림 |
slidingWindowType | COUNT_BASED | COUNT_BASED 또는 TIME_BASED로 호출의 결과를 저장하고 집계하기 위한 슬라이딩 윈도의 타입 |
slidingWindowSize | 100 | 서킷의 상태가 CLOSED일 때 요청의 결과를 기록하기 위한 슬라이딩 윈도의 크기 |
minimumNumberOfCalls | 100 | 서킷이 실패율(failure rate) 또는 지연된 응답(slow call rate)을 계산하기 전 요구되는 최소 요청의 수(슬라이딩 시간당) |
waitDurationInOpenState | 60000 [ms] | 서킷이 OPEN 에서 Half-open으로 변경되기 전 대기하는 시간 |
automaticTransition FromOpenToHalfOpenEnabled |
false | true라면 waitDurationInOpenState 기간이 지난 이후에 Open에서 Half-open으로 자동으로 상태가 변경되고 하나의 쓰레드가 CircuitBreaker의 모든 인스턴스들을 모니터링하며 상태를 확인 false라면 요청이 있을 때만 상태가 Half-open으로 변경된다. 즉, waitDurationInOpenState 기간이 지난 후에 새로운 요청이 있으면 Half-open 상태로 변경된다.모니터링을 위한 쓰레드가 필요없는 이점이 있다. |
recordExceptions | empty | 실패로 기록될 예외의 리스트로 실패율(failure rate)가 증가되는 예외 리스트 예외 리스트를 설정한다면, 다른 모든 예외는 ignoreExceptions에 의해 무시되지 않는다면 성공으로 간주 |
ignoreExceptions | empty | 성공 또는 실패로 기록되지 않는 예외들의 리스트 |
recordException | throwable -> true By default all exceptions are recored as failures. |
커스텀 Predicate로 예외가 실패로 기록될지 정의 예외가 실패로 카운트 되야 한다면 true를 리턴하고 성공으로 카운트 되야 한다면 false를 리턴 (ignoreExceptions에 의해 무시되지 않는 경우) |
ignoreException | throwable -> false By default no exception is ignored. |
커스텀 Predicate로 예외가 무시될지 정의 예외가 무시되려면 true를 리턴하고 실패로 카운트 되려면 false를 리턴 |
2. Bulkhead
동시에 실행될 수 있는 수를 제한합니다.
SemaphoreBulkhead Config
Config | propertyDefault | valueDescription |
maxConcurrentCalls | 25 | Bulkhead에 의해 허가된 동시 실행 최대 수 |
maxWaitDuration | 0 | 포화 상태의 Bulkhead에 진입하기 위해 block 되는 최대 시간 값이 0인 경우에는 바로 요청을 제한 |
FixedThreadPoolBulkHead Config
Config | propertyDefault | valueDescription |
maxThreadPoolSize | Runtime.getRuntime() .availableProcessors() |
최대 쓰레드 풀 크기 |
coreThreadPoolSize | Runtime.getRuntime() .availableProcessors() - 1 |
코어 쓰레드 풀 크기 |
queueCapacity | 100 | 큐의 크기 |
keepAliveDuration | 20 [ms] | 스레드 수가 코어보다 크면 초과된 유휴 스레드가 종료하기 전에 새 작업을 대기하는 최대 시간 |
3.RateLimiter
요청 수를 일정시간 동안 제한합니다.
Config | propertyDefault | valueDescription |
timeoutDuration | 5 [s] | 허가(Permission)을 위해 쓰레드가 대기하는 기본 시간 |
limitRefreshPeriod | 500 [ns] | Limit refresh 기간으로, 각 기간 이후에 RateLimiter가 일정 시간 동안 허가되는 요청 수를 다시 설정 |
limitForPeriod | 50 | 한 Limit refresh 기간 동안 허가되는 요청 수 |
4.Retry
실패한 요청에 대한 재시도 정책에 대한 조건을 설정합니다.
Config | propertyDefault | valueDescription |
maxAttempts | 3 | 최대 시도 횟수 |
waitDuration | 500 [ms] | 재시도 사이에 고정된 대기 시간 |
intervalFunction | numOfAttempts -> waitDuration | 요청 실패 이후 대기 간격을 수정하기 위한 함수이고 기본적으로는 대기 시간(waitDuration)이 일정하게 유지 |
retryOnResultPredicate | result -> false | 결과에 따라 재시도 여부를 결정하기 위한 Predicate를 설정 만약 결과가 재시도 되야 한다면, true를 리턴해야하고 그렇지 않다면 false를 리턴 |
retryOnExceptionPredicate | throwable -> true | 예외(Exception)에 따라 재시도 여부를를 결정하기 위한 Predicate를 설정 만약 예외에 따라 재시도 되야 한다면, true를 리턴해야하고 그렇지 않다면 false를 리턴 |
retryExceptions | empty | 실패로 기록되는 에러 클래스 리스트 즉, 재시도 되야하는 에러 클래스의 리스트 empty일 경우 모든 에러 클래스를 재시도 |
ignoreExceptions | empty | 무시되야 하는 에러 클래스 리스트 즉, 재시도 되지 않아야 할 에러 클래스 리스트 |
failAfterMaxRetries |
false | MaxRetries의 던지기를 활성화하거나 비활성화하는 부울초과됨재시도가 구성된 최대 시도 수에 도달했지만 결과가 여전히 재시도OnResultPredicate를 통과하지 못하는 경우 예외 |
5.TimeLimiter
요청 호출에 대한 소요되는 시간을 제한합니다.
Config | propertyDefault | valueDescription |
timeoutDuration | 1 [s] | Timeout 값 (기본 단위는 ms) |
cancelRunningFuture | true | Timeout 발생 후 future를 취소할지 결정하는 Boolean 값 |
circuit break 사용 방법만 작성합니다.
cloud-server와 cloud-service 중 coude-service에 해당하는 라이브러리로 cloud-resilience4j 1개로만 진행합니다.
1. 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-aop'가 추가되어야 circuit breaker를 사용할 수 있습니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
implementation 'io.micrometer:micrometer-registry-prometheus'
}
2. application.yml
server:
port: 7979
resilience4j:
circuitbreaker:
configs:
default:
registerHealthIndicator: true # actuator를 통해 circuitbraker 상태를 확인하기 위해 설정
minimumNumberOfCalls: 5 # Circuit Breaker가 에러 비율 또 slow call 비율을 계산하기 전에 요구되는 최소한의 요청 수
failureRateThreshold: 50 # 에러 비율 (퍼센트)로 해당 값 이상으로 에러 발생시 서킷이 Open 된다.
waitDurationInOpenState: 10s # 서킷의 상태가 Open에서 Half-open으로 변경되기 전에 Circuit Breaker가 기다리는 시간
instances:
testCircuitBreaker:
baseConfig: default
bulkhead:
instances:
testCircuitBreaker:
maxConcurrentCalls: 25 # 허가된 동시 실행 수를 25로 지정
maxWaitDuration: 0 # 포화 상태의 Bulkhead에 진입하기 위해 block 되는 최대 시간, 값이 0이므로 바로 요청을 막는다.
ratelimiter:
instances:
testCircuitBreaker:
limitForPeriod: 50 # limitRefreshPeriod 기간 동안 허용되는 요청 수
limitRefreshPeriod: 500ns # limit refresh 기간
timeoutDuration: 5s # 허가를 위해 쓰레드가 대기하는 기본 시간
registerHealthIndicator: true
# fallback method가 정의되어있지 않은 에러의 경우에만 재시도 한다.
retry:
instances:
testCircuitBreaker:
maxRetryAttempts: 3 # 최대 재시도 수
waitDuration: 500ms # 재시도 사이에 고정된 시간
# retryExceptions:
# Empty 일 경우 모든 에러 클래스에 대해 재시도
# - org.springframework.web.client.HttpServerErrorException
# - io.github.resilience4j.circuitbreaker.Exception
timelimiter:
instances:
testCircuitBreaker:
timeoutDuration: 1s # 원격 서버로부터 해당 시간안에 응답이 오는 것을 제한
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
shutdown:
enabled: true
health:
show-details: always # actuator에서 디테일한 정보들을 확인하기 위해 설정
health:
circuitbreakers:
enabled: true # actuator를 통해 circuitbraker 상태를 확인하기 위해 설정
3. Resilience4jContoller.java
package com.otrodevym.cloudresilience4j;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
@RequiredArgsConstructor
@RequestMapping("/resilience4j")
public class Resilience4jContoller {
private final TestService testService;
@GetMapping("/fail")
public Mono<String> fail() {
return testService.getFailHello();
}
@GetMapping("/success")
public Mono<String> success() {
return testService.getSucessHello();
}
@GetMapping("/data")
public Mono<String> data() {
return testService.getData();
}
}
4. TestService.java
package com.otrodevym.cloudresilience4j;
import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import io.github.resilience4j.retry.annotation.Retry;
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.Collections;
@Service
public class TestService {
private static final String TEST_CIRCUIT_BREAKER_NAME = "testCircuitBreaker";
@Bulkhead(name = TEST_CIRCUIT_BREAKER_NAME)
@CircuitBreaker(name = TEST_CIRCUIT_BREAKER_NAME, fallbackMethod = "fallback")
public Mono<String> getFailHello() {
return Mono.error(new CustomException("CustomException"));
}
@RateLimiter(name = TEST_CIRCUIT_BREAKER_NAME)
@Bulkhead(name = TEST_CIRCUIT_BREAKER_NAME)
@CircuitBreaker(name = TEST_CIRCUIT_BREAKER_NAME, fallbackMethod = "fallback")
public Mono<String> getSucessHello() {
return Mono.just("hello");
}
@TimeLimiter(name = TEST_CIRCUIT_BREAKER_NAME)
@Retry(name = TEST_CIRCUIT_BREAKER_NAME)
@CircuitBreaker(name = TEST_CIRCUIT_BREAKER_NAME, fallbackMethod = "fallback")
public Mono<String> getData() {
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8080")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080"))
.build();
webClient.get();
return webClient.method(HttpMethod.GET)
.uri("/")
.retrieve()
.bodyToMono(String.class);
}
public Mono<String> fallback(CustomException e) {
// e.printStackTrace();
return Mono.just("fallback");
}
public Mono<String> fallback(CallNotPermittedException e) {
// e.printStackTrace();
return Mono.just("CallNotPermittedException");
}
}
5. CustomException.javva
package com.otrodevym.cloudresilience4j;
public class CustomException extends Exception{
public CustomException(String customException) {
super(customException);
}
}
6. 설정 확인 및 테스트
6-1 actuator 확인
http://localhost:7979/actuator 에 접속해서 circuit을 찾으면 actuator에 추가 되어 있습니다.
6-2. actuator/health 확인
http://localhost:7979/actuator/health에 접속하면 circuit breaker의 상태를 확인할 수 있습니다.
6-3. /resilience4j/fail로 fallback 확인
.http를 이용해 5번에 오류를 발생시켜 close에서 open이 되도록 해보겠습니다.
우선 아래 빨간색으로 표시 된 부분은 4번 요청했고 3번 실패한 현재 상태를 표시하고 있고
5번 요청했을 때 실패율에 따라 close와 open을 결정합니다.
GET localhost:7979/resilience4j/fail
왼쪽은 CustomException이 발생하여 fallback으로 처리되는 결과입니다.
오른쪽은 빠르게 여러번 호출하여 제한이 걸린 결과입니다. 조금 후에 다시 시도 하면 다시 fallback으로 떨어집니다.
에러를 내고 확인해보면 state가 "OPEN"이 된것을 확인할 수 있습니다.
일정 시간이 지나고 나서 다시 에러를 발생시키면 "HALF_OPEN"이 된 것을 확인할 수 있습니다.
success로 일정 비율로 성공시키면 다시 close가 된것을 확인할 수 있습니다.