마이크로 서비스 아키텍처에서는 독립된 서비스 간 호출을 통해 전체 서비스를 제공한다. 그러다 보니 특정 서비스가 느려지거나 장애가 나게되면 다른 서비스들로 장애가 전파될 수 있다.
예를 들어 독립된 서비스 A, B 가 있을 때 B가 급격한 트래픽 증가로 응답속도가 매우 느려졌다고 가정해보자. 서비스 A가 서비스 B의 응답을 받은 후 요청을 완료할 수 있다고 하면, 서비스 A의 쓰레드는 서비스 B의 응답을 기다리느라 다른 요청을 처리하는데 지장이 갈 것이다. 만약 서비스 A를 호출하는 다른 서비스가 있다면, 서비스 B의 장애가 시스템 전체의 장애로 번질 가능성이 높아진다.
Circuit Breaker 패턴은 위의 문제를 해결할 수 있는 패턴이다.
회로에서 스위치가 닫혀있으면 전기가 공급되고, 스위치가 열려있으면 전기가 공급되지 않는 것처럼, 평소 서비스 B가 정상적으로 동작할 때는 스위치를 닫고 있다가, 서비스 B가 정상적으로 동작하지 않을 때는 스위치를 열어 애초에 서비스 B를 호출하지 않게 해서 서비스 A가 정상적으로 동작할 수 있게 하는 것이다.
이 때 무한정 스위치를 열고 있는 것이 아니라, 특정 시간 이후에 서비스 B가 원상 복구 되었을 때는 스위치를 닫아야 한다. 그래서 특정 시간 이후 일부 요청만 실행해보면서 기능이 다시 정상적으로 동작하는지 검증해야 한다.
따라서 Circuit Breaker의 상태는 총 3가지이다.
스위치가 Close된 상태에서, 타 서비스에 대한 요청의 실패들이 사전 정의된 임계값에 도달하면, 스위치가 Open된다. Open 된 상태에서 특정 시간이 지나면 Half-open 상태로 바뀌고 새로운 호출을 허용해서 타 서비스의 문제가 해결되었는지 확인하고, 만약 오류를 감지할 경우 다시 Open 상태로 전환시킨다. 오류가 사라졌다면 Close 상태로 돌아간다.
이 패턴을 구현한 대표적인 라이브러리는 자바 진영의 Netflix Hystrix 이다. 그러나 현재는 유지보수 되고 있지 않고, 그 뒤를 Spring Cloud 진영의 Reslience4j 가 있다.
Reslience4j 는 Netflix Hystrix에 영감을 받아 사용하기 쉽고 가벼운 fault tolearnce library이다. Circuit Breaker 외에도 Rate Limiter, Retry or BulkHead도 지원한다.
Reslience4j는 다음과 같이 Circuit Breaker를 정의할 수 있다.
// Create a custom configuration for a CircuitBreaker
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();
// Create a CircuitBreakerRegistry with a custom global configuration
CircuitBreakerRegistry circuitBreakerRegistry =
CircuitBreakerRegistry.of(circuitBreakerConfig);
// Get or create a CircuitBreaker from the CircuitBreakerRegistry
// with the global default configuration
CircuitBreaker circuitBreakerWithDefaultConfig =
circuitBreakerRegistry.circuitBreaker("name1");
몇 가지 주요 파라미터는 다음과 같다.
Parameter | Description |
---|---|
failureRateThreshold | 실패한 호출에 대한 임계값을 설정한다. 이를 초과하면 서킷이 열린다. |
slowCallRateThreshold | 느린 호출에 대한 임계값을 설정한다. 이를 초과하면 서킷이 열린다. |
slowCallDurationThreshold | "느린 호출"에 대한 기준값이다. |
waitDurationInOpenState | Circuit Breaker가 open 상태에서 half-open 상태로 전환할 때까지 기다리는 시간이다. |
permittedNumberOfCallsInHalfOpenState | half-open 상태에서 허용되는 요청 개수이다. |
minimumNumberOfCalls | Circuit Breaker를 동작시키는데 필요한 최소 요청이다. 예를 들어 이 값이 10이면 9개의 요청까지는 서킷브레이커의 상태 변경이 일어나지 않는다. |
파라미터를 뜯어보면, Circuit Breaker가 어떻게 동작하는지 더 자세하게 알 수 있다.
아래와 같이 Circuit Breaker를 타 서비스 호출에 적용할 수 있다.
CircuitBreaker circuitBreakerWithDefaultConfig =
circuitBreakerRegistry.circuitBreaker("name1");
List<OrderResponse> ordersList = circuitBreakerWithDefaultConfig.run(() -> orderServiceClient.getOrders(userId), throwable -> new ArrayList<>());
맨 마지막 즈음에 throwable → new ArrayList() 코드가 있어서, 실패 시 처리도 가능하다. 예제 코드는 사용자 서비스에서 사용자의 주문 정보를 주문 서비스로 요청하는 것인데, 만약 주문 서비스가 실패할 경우 빈 ArrayList를 리턴하도록 하여 실패를 처리할 수 있다.
Resilience4j는 Circuit Breaker 패턴을 훌륭하게 구현한 라이브러리이나, 현재 MSA 서비스들이 자바가 아닌 여러 다른 언어들로 작성되었다면 쓸 수가 없다. 그러면 매 언어마다 Circuit Breaker를 구현해야 할까? 물론 그럴 수도 있지만, 다른 방법이 있다. 사실, 생각해보면 Circuit Breaker와 같은 패턴은 MSA에서 발생하는 문제를 해결하기 위한 패턴이지, 모놀리식 관점에서는 크게 필요 없는 패턴이다.
애플리케이션은 특정 아키텍처의 도입으로 인한 패턴 및 코드가 아니라 해결하려는 도메인의 비즈니스 로직에 집중해야 한다.
Istio는 서비스 메시의 구현체이며, 쿠버네티스 환경에서 비즈니스 로직이 담긴 애플리케이션과 사이드카 패턴으로 배포가 가능하다.
Istio에서 envoy proxy는 애플리케이션 옆에 붙어서 애플리케이션의 트래픽을 앞 단에서 받고 제어할 수 있다. 여기에서 Circuit Breaker 패턴을 적용할 수 있다.
다음의 예제 설정을 통해 Circuit Breaker 패턴을 적용할 수 있다.
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: circuit-breaker-for-the-entire-default-namespace
spec:
host: "*.default.svc.cluster.local" # This is the name of the k8s service that we're configuring
trafficPolicy:
outlierDetection: # Circuit Breaker 동작 기준
maxEjectionPercent: 100 # 네트워크 차단 비율
consecutiveErrors: 2 # 연속적인 에러가 몇번까지 발생해야 Circuit Breaker가 동작할 것인지
interval: 5s #연속적인 에러가 interval에서 지정한 시간 내에 발생할 때 Circuit Breaker가 동작
위와 같이 DestinationRule을 인프라 단에 정의함으로써 Reslience4J 처럼 애플리케이션 코드 내부에 Circuit Breaker를 적용하지 않아도 된다.
MSA와 같은 분산 시스템에서는 단일 컴포넌트의 장애가 전체 컴포넌트의 장애로 이루어지지 않도록 방지하는 장치가 필요하다. Circuit Breaker 패턴은 해당 문제를 해결하여 MSA를 안정적으로 운영 가능하게 한다.