함수형 프로그래밍으로 설계된 경량(lightweight) 장애 허용(fault tolerance) 라이브러리이며 기존 Netflix Hystrix를 대체하는 라이브러리입니다.
Circuit Breaker, Rate Limiter, Retry, Bulkhead 등의 방식을 사용하고
이 방식들을 데코레이터 패턴으로 사용할 수 있도록 제공하여 필요한 방식만 직접 선택하여 사용할 수 있습니다.
호출이 실패하거나 타임아웃 상황에 Circuit Breaker 를 열어서 차단하는 방식

circuit breaker 상태
호출 결과를 저장하고 집계할 때 슬라이딩 윈도우 방식을 사용한다
실패 비율(failure rate) 또는 느린 호출(slow call) 이 설정한 임계치보다 크거나 같으면 CircuitBreaker의 상태는 Closed → Open 으로 변경된다
대기 시간이 경과하고 나면 OPEN → HALF_OPEN 상태로 변경되며 설정한 횟수만큼 호출을 허용해 이 백엔드가 아직도 이용 불가능한지, 아니면 사용 가능한 상태로 돌아왔는지 확인한다.
실패 비율이나 느린호출 비율이 설정한 임계치보다 크거나 같으면 상태는 다시 OPEN으로 변경되고 둘 모두 임계치 미만이며 CLOSED 상태로 돌아간다.
제한치를 넘어간 것을 감지했을 때의 동작이나 제한할 요청 타입과 관련된 광범위한 옵션을 제공한다.
간단히 제한치를 넘어선 요청을 거부하거나 큐를 만들어서 나중에 실행할 수도 있고, 어떤 방식으로든 두 정책을 조합해도 된다.
실패한 실행을 짧은 지연을 가진 후 재시도하는 매커니즘이다
동시 실행 횟수를 제한하는데 활용할 수 있는 패턴이다
~feign client에 circuit breaker 방식을 적용하였습니다~
아래 spring-cloud-starter-circuitbreaker-resilience4j 를 pom.xml에 추가합니다
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
그 외에 기능을 사용하고 싶으면 아래 디펜던시를 선택해서 추가하면 됩니다
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
<version>${resilience4jVersion}</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
<version>${resilience4jVersion}</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-retry</artifactId>
<version>${resilience4jVersion}</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
<version>${resilience4jVersion}</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-cache</artifactId>
<version>${resilience4jVersion}</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-timelimiter</artifactId>
<version>${resilience4jVersion}</version>
</dependency>
임의로 설정한 값들입니다.
resilience4j:
circuitbreaker:
configs:
default:
failureRateThreshold: 20 # 실패율 임계값 해당 값 이상이면 closed -> open으로 상태변경
permittedNumberOfCallsInHalfOpenState: 10 # half_open 상태일 때 허용할 call 개수
maxWaitDurationInHalfOpenState: 10000 # half_open 상태에서 open 상태로 변경되기 전 최대 유지시간 10초
slidingWindowType: COUNT_BASED # 슬라이딩 윈도우 타입 (COUNT_BASED / TIME_BASED)
slidingWindowSize: 50 # count_based - 개수 time_based - 초
minimumNumberOfCalls: 10 # 최초 failureRate, slowCallRate를 계산하기 위한 최소 Call 개수
waitDurationInOpenState: 60000 # open -> half open 상태로 변경 대기 시간
record-failure-predicate: com.test.config.resilience4j.TimeOutExceptionRecordFailurePredicate
| property | default value | description |
|---|---|---|
| failureRateThreshold | 50 | 실패한 호출에 대한 임계값으로 이 값을 초과하면 서킷이 열린다. |
| slowCallDurationThreshold | 60000 [ms] | 호출 시간이 설정한 값보다 길면 slow call로 판단한다. |
| slowCallRateThreshold | 100 | slow call 비율이 설정한 값보다 크거나 같으면 OPEN 상태로 바뀌고 호출을 차단한다. |
| minimumNumberOfCalls | 100 | failure/slow rate을 계산하기 위한 최소 호출 수 |
| waitDurationInOpenState | 60000 [ms] | OPEN → HALF_OPEN 으로 변경 되기까지 대기 시간 |
| recordExceptions | empty | 서킷 작동에 영향을 주는 예외 리스트 |
| 리스트에 등록된 예외 중에서만 서킷 작동에 영향을 준다. | ||
| ignoreExceptions | empty | 서킷 작동에 영향을 주지 않는 예외 리스트 |
| slidingWindowType | COUNT_BASED | 서킷브레이커가 닫힐 때 호출 결과를 기록하는데 사용하는 슬라이딩 윈도우의 유형 |
waitDurationInOpenState, permittedNumberOfCallsInHalfOpenState 작동 방식
1. 요청 50개중에 20%이상 실패나면 써킷 작동(open)한다.
2. 써킷 작동되면 waitDurationInOpenState(1분, default) 소요되면 half open으로 상태 변경된다.
3. half open 상태에서 permittedNumberOfCallsInHalfOpenState(10)개 만큼 수행후 성공하면 써킷 미작동(close)한다.
4. Half open 상태에서 permittedNumberOfCallsInHalfOpenState(10)개 만큼 수행후 실패하면 써킷 작동(open)한다.
❗️❗️ 만약 Half open을 허용하는 최대 유지시간(maxWaitDurationInHalfOpenState)이 지나면 permittedNumberOfCallsInHalfOpenState(10개)를 수행하기 전에 open 상태로 변경됩니다.
최대 유지시간보다 API 응답 대기 시간이 길다면 circuit이 무한으로 open 상태를 유지하는 상황에 빠질 수 있으므로 참고하여 maxWaitDurationInHalfOpenState을 선정해야 합니다.
maxWaitDurationInHalfOpenState값을 0(default)으로 지정하면permittedNumberOfCallsInHalfOpenState 수만큼 api 호출하여 판단하므로 default값을 유지하도록 하였습니다.
maxWaitDurationInHalfOpenState 설정 값만큼 대기한 이후
최소 호출 수만큼 요청을 받기 전에 maxWaitDurationInHalfOpenState에 설정된 값만큼 시간이 지나면 Circuit Breaker는 OPEN 상태로 전환합니다.
해당 시간 동안 받은 응답의 결과와 상관없이 무조건 OPEN 상태로 전환합니다.
출처) https://meetup.nhncloud.com/posts/385
특정 exception이 실패로 측정되도록 하는 Custom Predicate를 설정할 수 있습니다.
Predicate은 실패로 측정되고자 하는 exception은 true로, 성공으로 측정되고자 하는 경우는 false를 리턴해야 한다.
기본값에서는 모든 exception이 실패로 기록되지만 현 프로젝트에선 time-out이 발생하는 케이스만 오류로 측정하도록 하는 요구가 있었습니다.
public class TimeOutExceptionRecordFailurePredicate implements Predicate<Throwable> {
@Override
public boolean test(Throwable t) {
// RetryableException 예외이고, timed out 발생시에만 에러로 인식
if (t instanceof RetryableException && t.getMessage().startsWith("Read timed out")) {
return true;
} else if (t instanceof FeignException.GatewayTimeout) {
return true;
}
return false;
}
}
이와 같은 Predicate<Throwable>을 구현한 클래스를 생성하고 원하는 특정 예외인 경우 true값을 반환하도록 test 메서드를 오버라이드 하면 됨!
record-failure-predicate: com.test.config.resilience4j.TimeOutExceptionRecordFailurePredicate
위의 설정 yml에서 볼 수 있듯이 생성한 클래스를 record-failure-predicate 속성에 넣어주면 설정 완료 됩니다.
@FeignClient(name = "testClient", url = "tesetUrl")
public interface CommandClient {
@CircuitBreaker(name = "CommandClient#saveItem")
@PostMapping("/item")
ApiResponseDto<List<ItemResDto>> saveItem(@RequestBody List<ItemReqDto> itemReqDtos) {
...
}
@CircuitBreaker(name = "{circuitBreaker명}")
@CircuitBreaker(name = "{circuitBreaker명}", fallbackMethod = "{fallbackMethod 명}")
fallback Method를 지정할수도 있고 생략할 수도 있습니다.
circuitBreaker명은 클래스명#메서드명으로 설정했습니다.
원래 feign client에 circuitBreaker를 사용한다는 설정을 yml에 넣어두면 feign에서 자동으로 인식하여 메서드 별로 이름을 자동 매핑합니다. (DefaultCircuitBreakerNameResolver가 사용됨)
feign.client.circuitbreker.enabled = true 설정
Spring-Cloud-OpenFeign 4.0.0-SNAPSHOT 버전부터는
spring.cloud.openfeign.circuitbreaker.enabled
설정을 넣으면 @CircuitBreaker 어노테이션을 사용하지 않아도
feignClientClassName#calledMethod(parameterTypes) 패턴으로 자동 네임이 생성됩니다.
java.lang.IllegalArgumentException: Prometheus requires that all meters with the same name have the same set of tag keys.
There is already an existing meter named 'resilience4j_circuitbreaker_state' containing tag keys [name, state]. The meter you are attempting to register has keys [group, name, state].
CircuitBreakerNameResolver 인터페이스를 feign에서 가져와서 네이밍 규칙을 변경할 수 있습니다.
spring-docs 문서 참고하면 네이밍 규칙을 feignClientName_calledMethod의 형태로 다음과 같이 변경할 수 있습니다.
@Configuration
public class FooConfiguration {
@Bean
public CircuitBreakerNameResolver circuitBreakerNameResolver() {
return (String feignClientName, Target<?> target, Method method) -> feignClientName + "_" + method.getName();
}
}
CircuitBreakerRegistry는 설정값을 저장하는 인메모리 저장소라고 생각하면 됩니다
@RestController
@RequiredArgsConstructor
@Slf4j
public class CircuitBreakerTestController {
private final MarketCollectCommandClient feign;
private final CircuitBreakerRegistry circuitBreakerRegistry;
@GetMapping("/circuit/call") // feign client 호출
public ResponseEntity<List<ItemResDto>> call(@RequestBody List<ItemDto> itemDtos) {
List<ItemResDto> response = feign.saveItem(itemDtos).getData();
return ResponseEntity.ok(response);
}
@GetMapping("/circuit/close") // circuitBreaker close로 상태 변경
public ResponseEntity<Void> close(@RequestParam String name) {
circuitBreakerRegistry.circuitBreaker(name)
.transitionToClosedState();
return ResponseEntity.ok().build();
}
@GetMapping("/circuit/open") // circuitBreaker open으로 상태 변경
public ResponseEntity<Void> open(@RequestParam String name) {
circuitBreakerRegistry.circuitBreaker(name)
.transitionToOpenState();
return ResponseEntity.ok().build();
}
@GetMapping("/circuit/status") // circuitBreaker 상태 확인
public ResponseEntity<CircuitBreaker.State> status(@RequestParam String name) {
CircuitBreaker.State state = circuitBreakerRegistry.circuitBreaker(name)
.getState();
return ResponseEntity.ok(state);
}
@GetMapping("/circuit/all") // circuitBreaker 상태 확인
public ResponseEntity<Void> all() {
Seq<CircuitBreaker> circuitBreakers = circuitBreakerRegistry.getAllCircuitBreakers();
for (CircuitBreaker circuitBreaker : circuitBreakers) {
log.error("circuitName={}, state={}", circuitBreaker.getName(), circuitBreaker.getState());
}
return ResponseEntity.ok().build();
}
@ExceptionHandler(FeignException.class)
public ResponseEntity<?> handleFeignException(FeignException e) {
return ResponseEntity.badRequest()
.body(Collections.singletonMap("code", "FeignException"));
}
@ExceptionHandler(NoFallbackAvailableException.class)
public ResponseEntity<?> handleNoFallbackAvailableException(NoFallbackAvailableException e) {
return ResponseEntity.badRequest()
.body(Collections.singletonMap("code", "NoFallbackAvailableException"));
}
@ExceptionHandler(CallNotPermittedException.class)
public ResponseEntity<?> handleCallNotPermittedException(CallNotPermittedException e) {
return ResponseEntity.badRequest()
.body(Collections.singletonMap("code", "CallNotPermittedException"));
}
}
https://resilience4j.readme.io/
https://godekdls.github.io/Resilience4j/contents/
https://spring.io/projects/spring-cloud-circuitbreaker
https://docs.spring.io/spring-cloud-circuitbreaker/docs/current/reference/html/
https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign-circuitbreaker
https://arnoldgalovics.com/spring-cloud-feign-resilience4j-testing/
https://mangkyu.tistory.com/289
https://godekdls.github.io/Resilience4j/introduction/
https://meetup.nhncloud.com/posts/385