IT/AWS

Circuit Breaker 패턴

물통꿀꿀이 2018. 12. 20. 22:11

클라우드 환경에서 종종 등장하는 Circuit Breaker 패턴에 대해 알아보려고 한다.

보다 자세히 알고 싶다면 아래 URL을 확인하면 된다.

https://martinfowler.com/bliki/CircuitBreaker.html

* https://itnext.io/understand-circuitbreaker-design-pattern-with-simple-practical-example-92a752615b42

* https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern


클라우드 환경에서 서비스간 또는 프로세스간 원격 호출하는 것은 일반적이다. 

하지만 원격 호출의 문제는 호출이 실패했을 경우 timeout이 발생할 때까지 응답이 없다*는 것이다. 또한 응답이 없는 상황에서 외부 요청이 많아질 경우에는 내부 리소스가 부족할 수 있는 현상이 발생 할 수 있다.

* timeout이 없다면 무한 대기

따라서 Michael Nygard가 Circuit Breaker 패턴을 선보였다.


본격적인 예시를 확인하기 전에, Circuit Breaker 패턴의 기본 아이디어를 살펴보면 아래와 같다.

- 함수를 Wrapping하여 오류를 모니터링

- 장애가 특정 임계 값에 도달하면 Circuit Close


Flow는 다음과 같다.


그림 1. Without Circuit Breaker


그림 2. With Circuit Breaker


그림 1과 2의 차이점은 Service A에서 Service B를 호출 할 때, timeout이 발생한다는 것이다. 

특히 그림 1은 반환 값이 응답 데이터이지만 그림 2는 timeout이 발생했을 경우 timeout 에러로 반환된다는 점이다.


그림 3. Circuit State


또한 그림 3을 통해 Circuit의 상태를 확인 할 수 있다. 

- Closed : 정상적으로 동작 할 때 사용한다. (모든 호출이 서비스로 통과된다.) 그런데 실패 갯수가 임계치를 넘었을 경우 open 상태로 변경한다.

- Open : 함수를 실행시키지 않고 바로 error을 반환한다.

- Half-Open : timeout이 끝난 이후, 아직 문제가 발생하는지 확인하는지 테스트하기 위해 half-open 상태로 변경한다. 호출이 여전히 실패하면 다음에 다시 시도하고 성공하면 circuit을 원상태로 돌린다.


단순 예시일 뿐이지만 더 많은 이해를 위해 간단한 코드로 확인해보자.

async call(urlToCall) {
    // Determine the current state of the circuit.
    this.setState();
    switch (this.state) {
      case 'OPEN':
      // return  cached response if no the circuit is in OPEN state
        return { data: 'this is stale response' };
      // Make the API request if the circuit is not OPEN
      case 'HALF-OPEN':
      case 'CLOSED':
        try {
          const response = await axios({
            url: urlToCall,
            timeout: this.timeout,
            method: 'get',
          });
          // Yay!! the API responded fine. Lets reset everything.
          this.reset();
          return response;
        } catch (err) {
          // Uh-oh!! the call still failed. Lets update that in our records.
          this.recordFailure();
          throw new Error(err);
        }
      default:
        console.log('This state should never be reached');
        return 'unexpected state in the state machine';
    }
  }
let numberOfRequest = 0;

server.route({
  method: 'GET',
  path: '/data2',
  handler: (request, h) => {
    try {
      return h.response('data2');
    } catch (err) {
      throw Boom.clientTimeout(err);
    }
  },
});

const circuitBreaker = new CircuitBreaker(3000, 5, 2000);


server.route({
  method: 'GET',
  path: '/data',
  handler: async (request, h) => {
    numberOfRequest += 1;
    try {
      console.log('numberOfRequest received on client:', numberOfRequest);
      const response = await circuitBreaker.call('http://0.0.0.0:8000/flakycall');
      // console.log('response is ', response.data);
      return h.response(response.data);
    } catch (err) {
      throw Boom.clientTimeout(err);
    }
  },
});

간단한 2개의 코드 블록이지만 잘 살펴보면 routing하는 부분에서는 timeout 실패에 대한 예외처리를 하고 있다.

이렇듯 클라우드 환경에서 Circuit Breaker 패턴을 적용하면 fault tolerance 요구 사항을 확보할 수 있다. 

(즉, 사용자 측면에서 서비스 문제로 인한 발생하는 큰 불편함을 줄일 수 있을 것이다.)


하지만 Circuit Breaker는 자체적으로 좋아보기는 하지만 적용하기 위해서는 클라이언트 또한 fallback 응답에 대응해야 한다. 

그렇기 때문에 local data, cache data 등을 사용하기는 하지만 중요한 것은 Circuit Breaker를 통해 호출을 resilient하게 만들 수 있다는 것이다.