외부 API 연동 - 서킷 브레이커 설정

반응형

배경

PG사가 도입이 되었다. 원래 같으면 feign을 사용할지 restTemplate를 사용할지 결정만하고 그걸 사용했었던거 같다.
하지만 그리 간단한 문제가 아니었다. 서킷 브레이커라고 들어는 봤나??
사실 서킷 브레이커는 전기 쪽에서 나온 용어라 알고 있다. 서킷 브레이커를 한글로 다시 말하면 회로 차단기다.
실생활로 예시를 들면, 우리가 스위치를 통해 불을 키고 끄는것을 하게 되면 방에 불이 켜지고 꺼지고를 반복한다. 이것이 서킷 브레이커다.
하지만 설정들이 생각보다 헷갈렸다. 그것을 기록해 두면 조금더 편할거라 생각해 기록해둔다.

나의 설정

나의 설정은 다음과 같다.

resilience4j:
  circuitbreaker:
    instances:
    # 'pg-payment' 라는 이름의 서킷 브레이커 설정
      pg-payment:
        sliding-window-size: 10         # 최근 10번 호출 기록을 샘플로 사용
        failure-rate-threshold: 50      # 실패율이 50% 이상이면 서킷 오픈
        wait-duration-in-open-state: 10s  # 서킷 오픈 후, 다음 시도를 허용하기 전 대기 시간 10초
        permitted-number-of-calls-in-half-open-state: 3  # half-open 상태에서 최대 3회 호출 허용
        slow-call-duration-threshold: 10s  # 3초 이상 걸리면 느린 호출로 간주
        slow-call-rate-threshold: 50      # 느린 호출 비율이 50% 이상이면 서킷 오픈 조건 포함
        sliding-window-type: time_based
        record-exceptions:
          - java.lang.Throwable
      get-payment:
        sliding-window-size: 20
        failure-rate-threshold: 30
        wait-duration-in-open-state: 5s
        permitted-number-of-calls-in-half-open-state: 5
        slow-call-duration-threshold: 2s
        slow-call-rate-threshold: 50
        sliding-window-type: time_based
        record-exceptions:
          - java.lang.Throwable
  retry:
    instances:
      pg-payment:
        max-attempts: 3
        wait-duration: 1s
        exponential-backoff-multiplier: 2
        retry-exceptions:
          - java.lang.RuntimeException
          - java.util.concurrent.TimeoutException
          - feign.RetryableException
          - java.lang.IllegalArgumentException
      get-payment:
        max-attempts: 1
        wait-duration: 500ms
        retry-exceptions:
          - java.lang.RuntimeException
          - java.util.concurrent.TimeoutException
          - feign.RetryableException

이렇게 두 개의 인스턴스로 구분지은 이유는 update와 select를 구분을 짓고 싶었다.
두 개다 외부 API를 찌르는것은 맞다. 하지만 차이점은 하나는 업데이트용이고 또 하나는 조회다.
중요한 사실은 두개 모두 결제와 관련이 되어있는 사실을 명심해야 한다.

결제는 돈이다. 그렇기 때문에 속도보다는 정합성이 중요하다. 하지만 조회는 이미 업데이트가 된 이후다. 그렇기 때문에 모든 정보가 100%가져오는게 성공은 하지 않아도 된다. 만약, 가져오지 못한다면 또 가져오면 된다. 하지만 빠르게 정보를 가져오지 못한다면? 답답하지 않을까 생각이 든다.

속성별로 생각해보자.

그전에 sliding-window가 무엇일까?
sliding-window는 여러개의 창문을 움직이는?거라고 생각하면 된다.
pg-payment를 예시로 들면
sliding-window-size는 10으로 되어 있다. 이게 무슨 소리냐..하면 10개의 칸을 움직이면서 검사를 한다는 뜻으로 여겨진다.
아무래도 밑에 있는 속성들을 체크 하는듯 싶다.

그럼 하위도 생각해보자.

    failure-rate-threshold: 50      # 실패율이 50% 이상이면 서킷 오픈  
    wait-duration-in-open-state: 10s  # 서킷 오픈 후, 다음 시도를 허용하기 전 대기 시간 10초  
    permitted-number-of-calls-in-half-open-state: 3  # half-open 상태에서 최대 3회 호출 허용  
    slow-call-duration-threshold: 10s  # 10초 이상 걸리면 느린 호출로 간주  
    slow-call-rate-threshold: 50      # 느린 호출 비율이 50% 이상이면 서킷 오픈 조건 포함  
    sliding-window-type: time\_based  
    record-exceptions:  
      - java.lang.Throwable

일단 ai돌려서 나온결과는 위와 같다고 한다. 그럼 이걸 위 sliding-window-size와 빗대어 생각해보자.
failure-rate-threshold은 실패율이 50%이상이면 서킷을 오픈한다고 적혀있다. (이는 외부 API에 전기를 보내지 않는다는 뜻이 된다.)
그렇다는 얘기는 size가 10이니 사이즈가 5가넘어가는 순간 외부 API와의 통신을 안 한다는 뜻이 된다.

계속 쭉쭉 가보자.
wait-duration-in-open-state: 10s 이거 같은 경우는 서킷이 열린 경우를 생각해봐야 한다.
서킷이 열려있어도 통신은 계속되어야 한다. 서킷이 열려 있다고 해서 영원히 해당 API를 사용하지 않는다는 뜻은 아니기 때문이다.
이 말은 서킷이 열려있어도 10초 정도는 기달려 줄수 있다는 뜻이 된다.
만약, 외부 API와의 통신이 5초가 걸리고 서킷이 열린 상태이며 2초만 기다린다면 외부 API와 통신을 할 수 없게 된다. 왜냐하면 회로가 열려 있기 때문이다.

계속 말했듯이 이거는 결제 부분이다. 그렇다면 오래 기다리는것이 유리할까.. 우리가 어떤 매장에 갔는데 그 매장에서 카드 결제가 안된다고 상상해보자. 카드를 긁고 10분뒤에 문자가 온거랑 1초만에 온거랑 생각을 해보자. 물론 1초만에 완벽한 문자가 오면 좋겠지만 만약에 조금이라도 액수가 틀린다면? 난리가 난다. 하지만 10분뒤에 와서 액수가 그래도 괜찮다면 사람들은 대수롭지 않게 생각할거라 생각한다.
(물론 10분만에 기다렸는데 액수가 틀리면 더 난리날거다.) 이 말을 하는 이유는 정합성이 중요한 과정에서는 1초 2초 1분은 굉장히 짧은 시간이며 기다려 줄수 있다는 뜻이 된다. 하지만 결과를 보는게 한 세월 걸리는건 용납 못할거다. 걔네는 빠르게 재 요청을 해서 보내는것이 조금더 사용자 측면에서 좋다고 생각한다.

permitted-number-of-calls-in-half-open-state: 3 그다음은 요 친구다. 반만 열렸다는게 무슨 말일까?
서킷 브레이커에는 3가지 상태가 있다.

OPEN : API통신안함
CLOSE : API 통신함
HALF-OPEN : CLOSE하기전 준비 과정

서킷이 열리고 서서히 닫히는 과정에서 조금은 닫혀있는 상태가 있을 수 있다. 이것을 생각해보면
위 회로에서는 스위치가 서킷 브레이커 역할을 조금 하게 된다.
permitted-number-of-calls-in-half-open-state 이 상태는 우리의 프로그램이 이 정도는 버틸 수 있다는 뜻이 된다.
이것을 잘 작성하기 위해서는 아무래도 내 프로그램이 성능이 얼마나 되는지 정확히 알고 있는게 중요하는듯 싶다.

결제는 아무래도 보수적으로 잡는게 좋다고 생각한다. 즉, 하프 오픈 상태에서는 1개만 통과를 시키게 하는것도 나름 합리적인 판단이라 생각한다.
찾아보니 1은 너무 보수적이라고 한다.
3회는 "일시적인 문제를 걸러내면서도" 외부 서비스의 "진정한 복구 여부를 확인"하는 합리적인 횟수로 많이 채택됩니다. 즉, 3회 테스트를 통과하면 안전하게 통신을 재개해도 큰 무리가 없을 것이라는 판단을 내리는 것입니다. 즉, 3회정도가 나름 합리적인 횟수인듯 싶다.

1로 설정할 때의 위험성
permitted-number-of-calls-in-half-open-state를 1로 설정하는 것은 지나치게 보수적인 방법입니다.

일시적 네트워크 오류: 외부 API가 이미 복구되었는데, 단 한 번의 호출이 일시적인 네트워크 
불안정으로 실패할 수 있습니다. 
이 경우, 서킷 브레이커는 복구된 API를 고장난 것으로 오해하고 다시 OPEN 상태로 돌아가게 됩니다. 
이는 불필요한 서비스 차단으로 이어집니다.

느린 복구 시간: 이미 복구된 서비스임에도 매번 10초(wait-duration)를 기다린 후 
1회만 테스트하는 과정이 반복될 수 있습니다.

10으로 설정할 때의 위험성
반대로 10과 같이 너무 큰 숫자로 설정하는 것은 위험할 수 있습니다.

연쇄 장애 유발: 외부 API가 아직 불안정한 상태일 때 10개의 요청이 한꺼번에 몰리면, 
이는 오히려 API에 과부하를 주어 다시 완전히 다운시키는 '연쇄 장애'를 유발할 수 있습니다. 
이는 서킷 브레이커의 본래 목적인 장애 방지에 역행하는 결과를 낳습니다.

이제 거의다 왔다.

slow-call-duration-threshold

요거 같은 경우는 내 API에서 외부 API를 쏘게 될때 기다리는 시간을 뜻한다.
그러니까 내 API에서 외부 API에서 가는 거리를 운동장이라고 생각하고 그곳을 뛴다고 생각해보자.
그러면 그 시간이 나올거고 우리는 그것을 기록한다. 그리고 일반적으로 이 시간대면 느리다. 빠르다를 판단할 수 있게 된다.
이와 마찬가지이지 않을까? 그걸 얼마나 기다릴 수 있는지를 물어보는 거라 생각이 든다.
너무 느리면 내 API에도 영향이 있을 테니 신중히 결정하는게 좋을거 같다.
하지만 결제는 정합성이 중요하니 어느정도는 기다려 줄수 있지 않을까? 반면에 조회는 빠르게 동작을 해야 하니 조금만 느려도 바로바로 열어버리는것이 조금더 합리적이지 않나 싶다.

slow-call-rate-threshold 얘는 slow call를 얼마나 허용을 할까라 생각하면된다.
늦게 도착하면 서킷이 열려 통신을 할 수 가 없게 되어진다.
어떻게든 통신에 성공을 시키게 하려면 퍼센트를 높게 잡는게 좋을 듯 싶다.

뭔가 결제도 무조건 API에 성공을 시켜야지라는거와 거리가 좀 멀다고 생각해 오히려 퍼센트를 낮게 설정하는것이 좋을지도 모르겠다. 그러면서 재 결제를 유도하는것이 좋지 않을까??

다음은 sliding-window-type으로 내가 시간으로 판단할지 횟수로 판단할지 결정할 수 가 있다.
마지막으로 record-exceptions같은 어떤 예외를 실패로 기록할지 결정하는 수단으로 생각한다.

결론

서킷을 사용하게 되면 설정 파일에서 어떻게 설정을 해야 할지 감이 잡히지 않는거 같습니다. 이 속성들에 대해 어떻게 이해를 하는것이 중요하는듯 싶습니다. 리트라이는 정리는 하지 못했지만 어느 상황에서 리트라이를 하면 좋을지 고민하면 좋겠다고 생각이 듭니다.
그라파나로 보고 싶었지만

요런식으로 밖에 나오지 않아 조금은 아쉽습니다. 

댓글

Designed by JB FACTORY