MSA

CircuitBreaker,Resilience4J 구현

혀눅짱 2023. 9. 12. 13:48

user-service에서 사용자 주문정보를 가져오기 위해 order-service에 feignClient로 요청하게된다.

이때 만약 order-service가 매우 느려지거나 셧다운 된 상황 즉 장애가 발생한 상황이라고 가정한다면

user-service에서는 자기 서비스 에러가 아님에도 500에러를 도입하게된다.

장애가있는 마이크로서비스와의 통신을 사전 차단하여 장애가를 방지하는 역할이 바로 CircuitBreaker 이다.

 

user-service와 order-service 통신간에 CircuitBreaker는

정상적인 응답이 올경우에는 계속 CLOSE상태에 있다가 일정횟수이상 비정상적인 응답이올경우 OPEN상태로 변경된다.

CircuitBreaker 구현을 위해 Resilience4J를 사용하도록 한다.

 

Resilience4J는 Java 전용으로 개발된 경량화된 Fault Tolerance(장애감내) 제품
Resilience4J는 아래 6가지 핵심모듈.
- Circuit Breaker: Count(요청건수 기준) 또는 Time(집계시간 기준)으로 Circuit Breaker제공
- Bulkhead: 각 요청을 격리함으로써, 장애가 다른 서비스에 영향을 미치지 않게 함(bulkhead-격벽이라는 뜻)
- RateLimiter: 요청의 양을 조절하여 안정적인 서비스를 제공. 즉, 유량제어 기능임.
- Retry: 요청이 실패하였을 때, 재시도하는 기능 제공
- TimeLimiter: 응답시간이 지정된 시간을 초과하면 Timeout을 발생시켜줌
- Cache: 응답 결과를 캐싱하는 기능 제공

 

먼저 user-service에 디펜던시를 추가한다.

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
@Override
    public UserDto getUserByUserId(String userId) {
        UserEntity userEntity = userRepository.findByUserId(userId);

        if(userEntity == null){
            throw new UsernameNotFoundException("User not found");
        }

        UserDto userDto = new ModelMapper().map(userEntity, UserDto.class);

//        String orderUrl = String.format(env.getProperty("order-service.url"),userId);
//        ResponseEntity<List<ResponseOrder>> orderListResponse = restTemplate.exchange(orderUrl, HttpMethod.GET, null, new ParameterizedTypeReference<List<ResponseOrder>>() {
//        });
//
//        List<ResponseOrder> orders = orderListResponse.getBody();

        //List<ResponseOrder> orders = orderServiceClient.getOrders(userId);
        CircuitBreaker circuitbreak = circuitBreakerFactory.create("circuitbreak");
        List<ResponseOrder> orders = circuitbreak.run(()->  orderServiceClient.getOrders(userId), throwable -> new ArrayList<>());
        userDto.setOrders(orders);


        return userDto;
    }

서킷브레이커 인스턴스를 만들고 run메소드를 통해 정상적으로 order-service와 통신이되는 경우에는 주문정보를 받아오고

서킷브레이커가 오픈상태가되면 비어있는 list를 리턴받도록 하여 에러를 방지한다.

@Configuration
public class Resilience4JConfig {

    @Bean
    public Customizer<Resilience4JCircuitBreakerFactory> globalCustomConfiguration(){
        CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
                .failureRateThreshold(4)
                .waitDurationInOpenState(Duration.ofMillis(1000))
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
                .slidingWindowSize(2)
                .build();

        TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
                .timeoutDuration(Duration.ofSeconds(4))
                .build();
        return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
                .timeLimiterConfig(timeLimiterConfig)
                .circuitBreakerConfig(circuitBreakerConfig)
                .build()
        );
    }
}

서킷브레이커는 기본적으로 기본적으로 슬라이딩 윈도우를 사용해서 호출 결과를 저장하고 집계하여 circuit을 열거나 닫음

 

횟수 기반 슬라이딩 윈도우(Count-based sliding window) : 들어온 횟수를 기반으로 슬라이딩 윈도우를 잡는 방법입니다. N 크기의 circular array를 만들어서 해당 array의 실패률을 이용하여 circuit의 상태를 확정

 

시간 기반 슬라이딩 윈도우(Time-based sliding window): 시간을 기반으로 슬라이딩 윈도우를 잡는 방법입니다. N 초의 슬라이딩 윈도우 크기라면 N 개의 circular array를 만듭니다. 그리고 각 버킷에 특정 초에 발생한 호출의 결과를 집계하여 저장하고 있고 이를 초가 지남에 따라 밀어내고 가지는 형식

 

해당 서킷브레이커를 config에서 커스텀하여 옵션을 설정 할 수 있다.

 

 

failureRateThreshold ( 기본값 : 50 )

    • 실패율 임계값을 설정. 해당 %가 넘거나 같아지면 circuitBreaker의 상태가 Open으로 변경되며 실제 코드를 호출하지 않고 fallback 또는 fail 처리
  • slidingWindowType ( 기본값 : COUNT_BASED )
    • sliding window로 어떤 값을 사용할지 정함. 기본은 COUNT_BASED 이며 TIME_BASED로 사용할 수 있음
  • slidingWindowSize ( 기본값 : 100 )
    • sliding window 크기. COUNT_BASED라면 array 크기이며 TIME_BASED라면 초 
  • waitDurationInOpenState ( 기본값 : 60_000 [ms] )
    • circuit이 OPEN 상태가 되고나서 대기하는 시간