And Brain said,

Circuit Breaker, 순간아 멈추어라! 본문

IT/DevOps \ Architecture

Circuit Breaker, 순간아 멈추어라!

The Man 2023. 3. 14. 18:02
반응형

 
 
오늘은 MSA의 필수 패턴 중 하나인 Circuit Breaker에 대해 알아볼 것이다.
 
MSA(Microservice Architecture)는 애플리케이션의 유연성, 확장성, 유지보수성 등을 향상시키지만, 여러 개의 서비스가 함께 동작하기 때문에 각각의 서비스가 다운되는 상황이 발생할 수 있다.

이러한 문제를 해결하기 위해서 분산 시스템에서 일시적으로 실패한 서비스를 격리하고 복구하는 시스템 전체의 가용성을 높이기 위해 사용되는 디자인 패턴인 Circuit Breaker가 사용된다.
 
호출된 시스템이 지연이나 에러를 일으키면 Circuit Breaker가 해당 호출을 중지시키고 대체로직을 수행한다.

Circuit Breaker 패턴은 일반적으로 세 가지 상태를 가진다.

 

  • Closed: 서비스가 정상적으로 작동하고 있음을 나타내는 상태, 이 상태에서는 모든 호출이 서비스에 직접 전달된다. 서비스 호출이 성공하면, 서킷 브레이커는 상태를 유지하고, 호출이 실패하면 서비스에 대한 요청을 중단하고 Open 상태로 전환된다.

 

  • Open: 서비스가 실패했음을 나타내는 상태. 서비스에 대한 일정 시간 동안 연속된 실패가 발생하면, 서킷 브레이커는 자동으로 Open 상태로 전환된다. 이후 모든 호출은 서비스에 직접 전달되지 않고, 서킷 브레이커에서 바로 반환되는데, 이는 서비스에 대한 추가 요청을 보내지 않고, 서비스에 대한 부하를 줄이는 데 도움이 된다. Open 상태에서는 서비스의 상태를 모니터링하여 Half-Open 상태로 전환할 수 있다.

 

  • Half-Open: 서비스에 대한 요청을 일시적으로 전달하여 상태를 검증하는 상태. Open 상태에서 설정한 시간이 경과하면, 서킷 브레이커는 Half-Open 상태로 전환된다. 이후 첫 번째 요청은 서비스에 직접 전달되며, 서비스의 응답에 따라 서킷 브레이커는 다시 닫힐지 열릴지 결정한다. 만약 첫 번째 요청이 성공하면 Closed 상태로 전환되고, 실패하면 Open 상태로 전환된다.

 
Circuit Breaker는 이러한 세 가지 상태를 가지고 서비스를 모니터링하며, 실패를 감지하여 서비스에 대한 요청을 중단하고 부하를 줄이며 시스템 전체의 장애를 방지하고 안정적인 서비스 제공을 가능하게 한다.
 
 
자, 이제 본론으로 들어가자.
 
우리는 Spring Cloud Resilience4j를 사용할 것이다.

implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j' // circuit breaker
// https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:3.1.5'

호출할 클라이언트에 gradle 추가해주고

import java.time.Duration;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;

@Configuration
public class Resilience4JConfig {

    @Bean
    public Customizer<Resilience4JCircuitBreakerFactory> resilience4JCircuitBreakerFactoryCustomizer() {
        CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.TIME_BASED) // circuit breaker time 기반 처리
                .slowCallDurationThreshold(Duration.ofSeconds(10)) // 요청 지연으로 간주하는 시간
                .minimumNumberOfCalls(10) // 통계 최소 요청 건
                .build();

        return circuitBreakerFactory -> circuitBreakerFactory.configureDefault(
                id -> new Resilience4JConfigBuilder(id)
                        .circuitBreakerConfig(circuitBreakerConfig)
                        .build()
        );
    }
}

클라이언트에서 Circuit Breaker Bean 생성을 위한 Config를 만들어주자.
 
 
그리고 다른 서비스에 Circuit Breaker를 테스트하기 위한 실패 API를 만들어주자.
 

## portal-service

@PostMapping("/api/v1/circuit-breaker-test")
	public void successOrFail() {
		double randomNumber = Math.random();
		if (randomNumber >= 0.3) {
			System.out.println("Success");
		} else {
			throw new RuntimeException("Circuit Breaker");
		}
	}

 
그리고 이 API를 클라이언트에서 호출한다.

## user-service

@FeignClient(name = "portal-service")
public interface PortalServiceClient {
	
    @PostMapping("/api/v1/circuit-breaker-test")
    void successOrFail();
}

 
이제 클라이언트에서 대체로직을 위한 Fallback Factory 클래스를 만들어주자.
 

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class PortalServiceClientFallbackFactory implements FallbackFactory<PortalServiceClient> {
	@Override
    	public PortalServiceClient create(Throwable cause) {
        	return new PortalServiceClientFallback(cause);
    	}

    	public static class PortalServiceClientFallback implements PortalServiceClient {

        	private final Throwable cause;

        	public PortalServiceClientFallback(Throwable cause) {
            	this.cause = cause;
        	}
            
        	@Override
        	public void successOrFail() {
        		log.warn("Circuit Breaker : Seize the moment!");
        	}
    }
}

 
이제 다른 서비스 Controller에서 이 클라이언트 메소드를 호출해보자.

@RequiredArgsConstructor // final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성하여, 빈을 생성자로 주입받게 한다.
@RestController
public class UserApiController {

	private final CircuitBreakerFactory circuitBreakerFactory;
	private final PortalServiceClient client;
    
    @PostMapping("/api/v1/circuit-breaker")
	public void successOrFail() {
		CircuitBreaker circuitBreaker = circuitBreakerFactory.create("portal");
		circuitBreaker.run(() -> {
			client.successOrFail();
			System.out.print("success");
			return null;
		}, throwable -> {
			System.out.printf("Circuit Breaker : Stop moment!, ", throwable);
			return null;
		});
	}
}

 
이제 테스트를 해보자.
 

 

 
계속 호출 하다보면 Circuit Breaker가 열리고 그 순간 대체로직을 수행한다.
 

 

Thanks for watching, Have a nice day.

반응형
Comments