컨텐츠 바로가기

마틴 파울러가 작성한 CircuitBreaker 내용 해석

http://pulgrims.egloos.com/3047353

CircuitBreaker

Martin Fowler
6 March 2014

 일반적으로 소프트웨어 시스템에서는 각기 다른 프로세스들에서 동작하는 소프트웨어를 원격 호출하도록 한다. (각 프로세스는 네트워크 상에서 각기 다른 장비에 존재할 것이다.) 메모리 상에서의 호출과 원격 호출의 큰 차이점 중 하나는 원격 호출은 Fail이 일어날 수 있거나 어떤 타임아웃 제한에 다다를 때까지 Response를 주지 않고 Hang이 걸릴 수 있다는 것이다. 응답없는 supplier에 많은 호출자가 묶여있다면 어떤 것이 문제가 될까? 중요 자원이 바닥 나서 여러 시스템에 걸쳐 연쇄 Failure을 일으킬 수 있다. Michael Nygard는 Release It라는 책을 통해 이러한 치명적인 연쇄 Fail을 방지할 수 있는 Circuit Breaker pattern을 대중화하였다. 

 circuit breaker의 기본적인 개념은 매우 단순하다. failure를 모니터링하는 circuit breaker object에 protected function call을 wrapping한다. failure가 특정 임계치에 다다르면, circuit breaker가 trip되고 뒤에 추가로 발생하는 circuit breaker을 호출하는 모든 call들은 protected call이 전혀 이루어 지지않고 에러를 리턴한다. 또한, 일반적으로 circuit breaker가 trip되면 어떤 모니터링 경고가 나타나길 원할 것이다.


여기 타임아웃에 대한 Failure를 방지하기 위한 Ruby로 만든 예제가 있다.
protected call을 하는 Lambda함수 block을 가진 breaker를 셋팅하였다.

cb = CircuitBreaker.new {|arg| @supplier.func arg}

 breaker는 block을 저장하고 여러 파라미터(임계치, 타임아웃 시간, 모니터링)를 초기화하고, reset함수를 통해 closed 상태로 만든다.

class CircuitBreaker...
  attr_accessor :invocation_timeout, :failure_threshold, :monitor
  def initialize &block
    @circuit = block
    @invocation_timeout = 0.01
    @failure_threshold = 5
    @monitor = acquire_monitor
    reset
  end
  
circuit breaker가 호출된 후, circuit이 closed 상태라면 기본 block이 호출될 것이고, open 상태라면 에러를 리턴할 것이다.

# client code
    aCircuitBreaker.call(5)


class CircuitBreaker...
  def call args
    case state
    when :closed
      begin
        do_call args
      rescue Timeout::Error
        record_failure
        raise $!
      end
    when :open then raise CircuitBreaker::Open
    else raise "Unreachable Code"
    end
  end
  def do_call args
    result = Timeout::timeout(@invocation_timeout) do
      @circuit.call args
    end
    reset
    return result
  end
  
타임아웃이 발생한다면, failure count가 증가하고 성공한다면 reset을 통해 count를 0으로 돌려놓을 것이다.

class CircuitBreaker...
  def record_failure
    @failure_count += 1
    @monitor.alert(:open_circuit) if :open == state
  end
  def reset
    @failure_count = 0
    @monitor.alert :reset_circuit
  end
  
임계치를 failure count와 비교해나가면서 breaker의 상태를 결정하게 된다.

class CircuitBreaker...
  def state
     (@failure_count >= @failure_threshold) ? :open : :closed
  end
  
 이 간단한 circuit breaker는 circuit이 open 상태가 되면 protected call이 발생하는 것을 방지하지만 다시 잘 동작할 경우에는 다시 reset할 수 있는(closed 상태로 되돌릴 수 있는) 외부 개입이 필요할 것이다. 빌딩에서는 전기 회로 차단기가 적절한 접근 방식이지만, 소프트웨어 circuit breakers의 경우, 스스로 underlying calls(protected call)이 다시 잘 동작하는지 확인할 수 있는 breaker가 적절할 것이다. 적절한 interval 뒤에 protected call을 다시 호출하여 성공하면 breaker가 reset되도록 하면 이러한 self-resetting behavior를 구현할 수 있다.


 이러한 breaker를 만들기 위해 reset을 시도하기 위한 임계치를 추가하고 마지막 에러가 발생한 시간을 가지고 있는 변수를 세팅해야한다.

class ResetCircuitBreaker...
  def initialize &block
    @circuit = block
    @invocation_timeout = 0.01
    @failure_threshold = 5
    @monitor = BreakerMonitor.new
    @reset_timeout = 0.1
    reset
  end
  def reset
    @failure_count = 0
    @last_failure_time = nil
    @monitor.alert :reset_circuit
  end
  
 circuit breaker의 세번째 상태 half open은 circuit이 프로그램이 수정되었는지 확인하는 trial call을 할 준비가 되었다는 것을 의미한다. 

class ResetCircuitBreaker...
  def state
    case
    when (@failure_count >= @failure_threshold) && 
        (Time.now - @last_failure_time) > @reset_timeout
      :half_open
    when (@failure_count >= @failure_threshold)
      :open
    else
      :closed
    end
  end
  
 half-open 상태에서 call이 요청되면 trial call이 발생한다. 성공하면 breaker를 reset하고 아니라면 timeout 오류가 또 발생할 것이다.

class ResetCircuitBreaker...
  def call args
    case state
    when :closed, :half_open
      begin
        do_call args
      rescue Timeout::Error
        record_failure
        raise $!
      end
    when :open
      raise CircuitBreaker::Open
    else
      raise "Unreachable"
    end
  end
  def record_failure
    @failure_count += 1
    @monitor.alert(:open_circuit) if :open == state
    @last_failure_time = Time.now
  end
  
 이 예제는 단순히 설명을 위한 것이다. 실제 circuit breakers는 더 좋은 기능과 파라미터 설정을 제공한다. 그것들은 network connection failures 같은 protected call 발생시킬 수 있는 다양한 에러를 막아줄 것이다. 모든 에러에 대해 circuit이 trip되야하는 것은 아니다. 어떤 에러들은 정상적인 failures로 반영해야 하고 정규 로직의 일부분으로 다뤄야한다.

 트래픽이 많아지면 초기 타임아웃때문에 계속 기다리고 있는 call들이 많이 생기는 문제가 발생할 수 있다. 원격 호출은 느린 경우가 많아서, come back하면(처리되고 나면) 결과를 다루도록 future or promise(병렬처리 디자인 패턴)를 사용하여 각기 다른 스레드에서 각각 call하도록 하는 것이 좋다. thread pool에서 이러한 thread를 얻어냄으로써, thread pool이 고갈났을 때 중단하기 위한 circuit을 준비할 수 있다. 
(역자 주: multi-thread를 통해 빠르게 thread 고갈을 파악하여 혹은 thread 갯수(임계치)를 넘어서면 더 이상 call이 되지 않도록 한다)

 예제는 breaker가 trip되는 간단한 방법을 나타낸다. failure count 변수는 call이 성공하면 reset된다. 더 복잡한 접근 방법은 에러의 빈도를 확인하여, 예를 들어, 50% failure rate가 될 때마다 trip시키는 것이다. 타임아웃에 대해서는 10, 연결 실패에 대해서는 3으로 설정하는 것처럼 각기 다른 에러에 대한 임계치를 설정할 수도 있다.

 여기 제시한 예제는 동기 call에 대한 circuit breaker이다. 하지만 circuit breaker는 비동기 통신에서도 유용하다. 일반적인 기술은 supplier가 일정한 속도로 소비하는 queue에 모든 requests를 넣는 것이다. 이것은 서버 오버로드를 피하기 위한 유용한 기술이다.
이 경우 queue가 가득찰 때 circuit break가 발생한다.

 독자적으로, circuit breakers는 실패할 가능성이 있는 작업에 엮여있는 자원을 줄여준다. client가 타임아웃을 기다리는 것을 방지하고, 중단된 circuit은 힘겨워하는 서버에 부하를 주는 것을 방지한다. 나는 여기서 circuit breakers에서 일반적인 경우인 원격호출에 대해서 이야기하고 있지만 시스템의 어떤 파트로 부터 발생한 failure로 부터 다른 파트를 보호하길 원하는 어떤 상황에서도 사용될 수 있다. Circuit breakers는 모니터링하기에 좋은 장소이다. breaker 상태의 어떤 변화라도 기록되어야 하고 breaker는 더 심도있는 모니터링을 위해 그들의 상태 세부사항을 드러내야 한다.

 Breaker의 행동은 해당 환경에서의 심도 깊은 문제들에 대해 경고를 주기 위한 좋은 자료가 되는 경우가 많다. 작업 스태프는 breakers를 trip하거나 reset할 수 있어야 한다. Breakers는 그 자체로 가치가 있지만 그것을 사용하는 clients이 breaker failures에 반응해야할 필요가 있다. remote invocation과 마찬가지로, failure가 발생한 경우에도 무엇을 해야할지 고려해야 한다. 그것이 당신이 수행하고 있는 작업을 실패시켰는가? 혹은 당신이 할 수 있는 차선책이 있는가? 신용카드 인증은 나중에 다루기 위해 queue에 들어갈 수 있다. 어떤 데이터를 얻는 데 실패한다면 어떤 오래된 데이터를 보여줌으로써 완화될지도 모른다. 물론 그 데이터는 보여주기에 충분히 양호해야 한다.

추가 참고 사항

 netflix 기술 블로그에는 많은 서비스를 가진 시스템의 신뢰도를 쌓기 위한 많은 유용한 정보가 담겨있다. Dependency Command에서는 circuit breakers을 사용하는 것과 thread pool limit에 대해 이야기하고 있다. Netflix는 분산 시스템에 대한 latency와 fault tolerance를 다루기 위한 정교한 도구인, Hystrix를 가지고 있다. Hystrix는 thread pool limit을 사용하여 circuit breaker pattern을 구현하고 있다.
 그 밖에도 circuit breaker pattern을 Ruby, Java, Grails Plugin, C#, AspectJ, Scala로 구현한 도구들이 오픈 소스로 존재한다.



덧글|신고