2022. 12. 28. 23:58ㆍBACKEND
Circuit Breaker
마이크로 서비스 아키텍처(MSA, Micro Service Architecture)를 쉽게 말하면, 한가지 일만 잘하는 서비스들이 협업하는 아키텍처입니다. 서비스들은 HTTP 통신이나 RPC, Socket 등으로 서로 간의 통신하여 협업합니다.
많은 경우에서, 마이크로 서비스들은 특정 기능을 제공하기 위해 해당 아키텍처의 다른 서비스에게 통신을 요청합니다. 그리곤, 원하는 응답을 받아 계속해서 처리하죠. 비동기의 경우에는 달라질 수 있지만, 동기 방식에는 이런 방식으로 각 서비스가 의존적으로 구성됩니다.
이 경우의 문제점은 한 서비스에서 에러가 발생하거나 느려지면 이를 호출하는 다른 서비스들로 장애가 전파된다는 것입니다. 이를 위해 마이크로 서비스 아키텍처의 서비스들은 스스로 회복성(Resilience)를 가지도록 구성되어야 합니다.
Fault Tolerance
단어에서 의미하는 바를 직역하면 장애 저항력, 즉 장애나 결함에 대한 내성으로 해석할 수 있습니다. 소프트웨어에서의 Fault Tolerant 란 시스템이 장애를 겪을 때 해당 장애가 그 서비스에 영향을 끼치지 않는 능력을 의미합니다. 유저가 서비스를 사용하던 중 오류가 발생했을 때 어떻게 대처해서 자연스러운 유저 플로우를 제공하냐의 능력을 의미할 수 있습니다. 견고한 시스템을 설계하기에 이러한 Fault Tolerant는 빠질 수 없습니다.
서비스를 개발할 때는 보이지 않던 오류들이 라이브 환경에서 많이 발생하곤 합니다. 테스트 환경과 실제 환경의 격차를 줄이지 못하는 한계 때문이죠. 본인의 서비스에서 다양한 오류나 예기치 못한 예외들이 없다고 말할 수 있을까요? 본인이 개발한 제품에서 예외가 존재함을 인정, 그리고 인식해서 어딘가에서 발생할 오류를 어떻게 대응할지를 처리해야합니다. 이렇게 본인의 제품에 Fault Tolerant 를 높여가야 합니다.
Circuit Breaker의 개념은 굉장히 간단합니다. 보호하고자 하는 요청을 Circuit Breaker 객체에 감싸서 실패했는지 성공했는지를 모니터링합니다. Circuit Breaker는 등록된 서비스의 활성 여부를 확인하면서, 지속된 실패 비율이 특정 임계치에 도달하게 되면 그 이후의 모든 요청을 오류 혹은 Fallback 함수로 대처됩니다.
Service A, B, C: Client-side 서비스
Service D: 구매한 주문 관리 서비스
Service F: 사용자 관리 서비스
Service A, B, C는 Service D에게 HTTP 통신을 요청하면서 의존하고,
Service D는 Service F에게 통신을 요청하며 의존합니다.
위 순서도에 설명을 덧붙이자면, Service A, B, C는 Service D에게 "UserA의 구매 관리 이력"을 요청합니다.
순서와는 관계없이 Service D는 Service F에게 "UserA가 가진 유저 정보"를 요청합니다.
이때, Service F에 장애가 발생했다고 가정해봅시다.
Service D가 Service F에게 요청을 보냈을 때 Service F의 오류로 Service D에게 정상적인 응답을 주지 못했습니다. Service D는 그에 상응하는 로직이 정상 처리되지 못하고 오류가 발생할 수 있습니다. 이 때, Service A, B, C가 Service D로 요청을 보내고, Service D의 오류로 제대로된 응답을 주지 못합니다. 이에 따라 Service A, B, C는 오류가 전파됩니다.
한 서비스에서 장애가 발생하면 그에 의존하는 서비스들에게 그 장애가 전파될 수 있습니다. 오류가 발생하면 Retry나 Fallback 메소드를 설정할 수 있습니다. 하지만, 이에 대해서도 부족한 점이 있죠. 이럴 때 Circuit Breaker를 사용해서 적절한 Fault Tolerance를 구현할 수 있습니다.
Circuit Breaker
마이크로 서비스 아키텍처는 장애 전파를 막기 위해 Circuit Breaker 패턴을 사용하며, 다른 서비스로 요청이 실패하면 임계치에 따라 이후부터는 빠른 실패 처리를 수행합니다.
Scenario Example
아래와 같은 서비스 A, B 가 있다고 가정해보겠습니다.
A는 B에게 HTTP 요청을 하며, Http Client는 3번의 Retry를 갖습니다.
서비스 A가 서비스 B에게 요청을 보내는데, 서비스 B에 장애가 발생합니다.
서비스 A는 서비스 B가 정상 응답을 주지 않는, 가령 타임아웃 등의 문제를 깨닫고 3번의 Retry를 합니다.
모든 Retry 를 실패했을 경우, Circuit Breaker는 개발자에 의해 지정된 fallback 메소드를 실행합니다.
다시 서비스 A가 서비스 B에게 요청을 보내고, 위와 동일한 과정으로 fallback 메소드를 실행한다고 합시다.
이렇게 실패가 여러번 지속되어 실패 비율이 특정 임계치를 넘어가게 되면, Circuit Breaker는 OPEN 상태로 변합니다. 이금 부터 사전에 정의된 reset time동안 더 이상의 서비스 B로 호출을 하지 않습니다.
“Everything fails all the time” — Werner Vogels
위와 같이 실패를 빠르게 받아들이고 어떻게 처리할지를 고려해보는 방식이 바로 Fault Tolerance 입니다. Circuit Breaker는 실패를 받아들인다는 전제하에, 실패 상황을 위해 빠르게 대처할 수 있도록 MSA 구조에서 사용됩니다.
Circuit Breaker 동작 원리
Circuit Breaker는 Clien와 Supplier 사이에서 요청과 응답에 대한 통신을 조율합니다. Supplier에 장애가 발생하지는 않았는지, 만약 장애 발생의 전체 통신을 고려한 비율이 최대 실패 임계치를 넘지는 않았는지를 확인합니다.
위의 그림을 아래와 같은 과정으로 정리해볼 수 있습니다.
- Client 서비스에서 Supplier 서비스로 요청
- Supplier 장애 없음: Circuit Breaker는 요청을 그대로 전달 ← Circuit [Closed]
- Supplier 장애 발생: Circuit Breaker는 Supplier 서비스로의 요청 차단
- Fallback으로 지정한 응답을 Client 서비스로 대신 전달
- 특정 임계치 이상 Supplier의 장애 지속 ← Circuit [Open]
- ...
- Circuit Breaker의 Open 상태를 Closed로 변경 (사전에 지정된 시간 측정) ← Circuit [Closed]
Status
🔗 Microsoft Azure Circuit-Breaker Reference
CircuitBreaker는 Finite State Machine으로 구현되며, 상태 값에 따라 동작됩니다. Finite State Machine에는 세 개의 상태를 가집니다: CLOSED, OPEN, HALF_OPEN.
- CLOSED
- Entry: failure counter 초기화
- Do: 만약 수행 성공 시 그 결과를 반환하고, 수행에 실패했다면 failure counter를 증가시킨 후 failure 반환
- 실패 횟수가 임계 값 도달 시 → Open
- OPEN
- Entry: timeout timer 실행 시작
- Do: failure 반환
- Timeout 만기 시 → Half-Open
- HALF_OPEN
- Entry: success counter 초기화
- Do: 만약 수행 성공 시 success counter를 증가시킨 후 그 결과를 반환하고, 수행에 실패했다면 failure 반환
- 성공 횟수가 임계 값 도달 시 → Closed
- 수행 실패 시 → Open
또한, 아래 두 개의 특별한 상태를 가질 수 있습니다.
- DISABLED: 항상 접근 허용
- FORCED_OPEN: 항상 접근 거부
위 두 상태는 Circuit Breaker 이벤트를 생성할 수 없으며, Metric 정보를 수집할 수 없습니다.
이러한 상태를 종료하기 위해서는(1) '상태 변경을 트리거'하거나 (2) 'Circuit Breaker를 리셋'시키는 방법이 있습니다.
CircuitBreaker는 호출 결과를 저장하고 집계하기 위한 sliding window를 사용합니다.
- count-based sliding window: 마지막 N개의 호출 결과를 집계
- time-based sliding window: 마지막 N 초의 호출 결과를 집계
Sliding window
Sliding window는 N 사이즈measurements의 원형 배열로 구현됩니다. count window의 크기가 10라고 하면, 원형 배열에는 항상 10개mesurements를 가집니다. sliding window은 총 집계를 점진적으로 업데이트합니다. 총 집계는 새로운 호출 결과가 기록될 때 업데이트됩니다. 가장 오래된 측정 값이 제거evicted될 때, 측정 값은 총 집계에서 차감되고 버킷은 리셋됩니다. (Substract-on-Evict) 스냅샷이 미리 집계되어 있고 윈도우 사이즈와 무관하기 때문에, 스냅샷 검색 시간은 O(1) 입니다. 구현체가 필요로 하는 공간(메모리 소비)은 O(n)입니다.
예시) slidingWindowSize = 10, minimumNumberCalls = 6, failureRateThreshold = 50
: 위의 속성 값의 의미는 실패율을 계산하기 위한 최소 횟수는 6회입니다. 처음 다섯 번만 호출되는 경우 최초 상태인 CLOSED 상태에서 변경되지 않습니다. 6번째 호출된 이후부터 카운팅하며, 만약 호출이 6회 요청되었을 때 실패가 4번인 경우 4/6이기 때문에 실패 확률은 67%가 됩니다. 이 때, 실패 확률failureRateThreshold이 50(%) 이상(>=)이므로, 임계치 이상의 값을 가지기 때문에 OPEN 상태로 변경됩니다.
Count-based sliding window
: 요청 시간이 N번 실패했을 때 실패율(Failure Rate)가 설정 값(failureRateThreshold) 보다 클 경우 Circuit Breaker의 상태를 OPEN하는 방식
Time-based sliding window
: 요청 시간이 N 시간을 초과하는 비율 (Slow Call Rate)가 설정 값(slowCallRateThreshold) 보다 클 경우 Circuit Breaker의 상태를 OPEN하는 방식
Resilience4J
Resilience4j는 Spring Cloud에서 Circuit Breaker를 비롯한 다양한 Fault Tolerant를 위한 모듈을 사용할 수 있도록 제공합니다.
Fault Tolerance: Resilience
Resilience는 '회복 탄력성'입니다. 회복 탄력적이게 된다는 의미는 소프트웨어가 예상 밖의 곤란한 상황에서 빠르게 대처하거나 회복할 수 있는 능력을 의미합니다. 소프트웨어에서 회복 탄력성은 'Fault Tolerance' 즉, 장애 내구성을 실현하는 하나의 방식입니다. Fault Tolerance 시스템은 장애가 발생해도 해당 서비스를 정상적이거나 부분적으로 사용할 수 있게 제작하는 시스템입니다. Circuit Breaker는 오류를 대처하고, 막고, 피함으로써 Fault Tolerance를 구현합니다.
✔️ Circuitbreaker
CircuitBreaker는 단어 그대로 회로 차단기입니다. 흔히 우리가 과학 시간에 배운 회로 차단기를 떠올려보면 회로 사이 전류가 흐르는 회로 차단기가 있죠. 회로 차단기를 닫으면 닫힌 상태에서는 전류가 흐르고, 열린 상태에서는 회로의 흐름이 끊겨 전류가 흐르지 않습니다.
CircuitBreakerRegistry
CircuitBreaker instance를 만들고 없애는 관리를 할 수 있는 class입니다. 기본적으로 ConcurrentHashMap을 활용한 in-memory CircuitBreakerRegistry를 제공합니다.
✔️ Rate limiter
CircuitBreaker와 비슷한 API를 가지고 있습니다. In-memory RateLimiterRegistry를 제공하고 RateLimiterConfig로 설정할 수 있습니다.
✔️ Bulkhead
Hystrix와 다른 점은 Hystrix는 각각의 dependecy 마다 Thread pool을 주고 Thread Pool size만큼 execution을 제한하고 있다면 resilience4j-bulkhead는 maxConcurrentCalls을 semaphore의 permits 값으로 주고 semaphore를 이용해 제한합니다.
다른 구현 모듈과 비슷하게 In-memory BulkheadRegistry를 제공합니다.
✔️ Retry
: Automatic retrying (sync and async)
Retry는 Registry는 따로 없고 Retry retry = Retry.ofDefaults("id"); 이런 식으로 생성합니다.
✔️ Cache
resilience4j-cache는 javax.cache.Cache를 wrapping해서 사용합니다. javax.cache.Cache instance를 만들고 그 instance를 io.github.resilience4j.cache.Cache로 wrapping합니다.
✔️ TimeLimiter
TimeLimiter는 future supplier의 time limit을 정하는 API입니다.
Options
Reference : (github) CircuitBreakerConfig
Resilience4j를 사용할 때, CircuitBreaker와 Retry 설정이 필요합니다. 아래의 표는 Resilience4j 모듈 중 개념 이해에 도움이 될 만한 옵션 값을 정리한 내용입니다. 해당 내용을 참고해서 적절히 장애 상황을 대비할 수 있습니다.
OPTION | DESC | VALUES | DEFAULT | MODULE |
limitForPeriod | 제한 갱신 후 리소스를 사용할 수 있는 권한permissions 개수 (limitRefreshPeriod 값으로 지정된 한 번의 RateLimiter 기간 동안 사용 가능한 권한 수) | (int) | 50(permissions) | Rate limiter |
limitRefreshPeriod | 한 번의 제한 리프레시 기간을 설정 각 기간이 지나면 rate limiter는 권한의 수를 limitForPeriod 값으로 재설정 (Resilience4j 자체에서는 nanoseconds를 사용하며 shenyu 는 milliseconds 로 변환하여 사용) | (java.time.Duration) | 500(ms) | Rate limiter |
timeoutDurationRate | 권한을 다 사용했을 때, 스레드가 다음 권한을 기다리는 대기 시간을 설정 (Resilience4j 자체에서는 nanoseconds를 사용하며 shenyu 는 milliseconds 로 변환하여 사용) | 5000 (ms) | Rate limiter | |
timeoutDuration | CircuitBreaker 요청 대기 시간을 설정 | (long) | 30000(ms) | Time Limiter (Circuit Breaker) |
automaticHalfOpen | Open 상태에서 Half-Open 상태로 자동 전환 적용 여부 설정 | true: ON, false: OFF (java.lang.Boolean) | false | Circuit Breaker |
circuitEnable | Circuit Breaker 사용 여부 설정 | 0: OFF , 1: ON (int) | 0 | Circuit Breaker |
fallbackUri | 폴백 URI를 설정 | (java.time.String) | - | |
slidingWindowSize | CircuitBreaker가 닫혀있을 때, 호출의 결과를 기록하는데 사용되는 sliding window의 사이즈를 설정 | (int) | 100 | Circuit Breaker |
slidingWindowType | CircuitBreaker가 닫혔을 때 호출 결과를 기록하는 데 사용되는 Sliding window의 유형을 구성 | 0: count-based, 1: time-based (int) | 0 (count-based) | Circuit Breaker |
minimumNumberOfCalls | 최소 호출 값을 설정 CircuitBreaker가 오류 율이나 슬로우 호출 비율을 계산하기 전에 (sliding window period 당) 요구되는 호출 | (int) | 100 (counts) | Circuit Breaker |
waitIntervalInOpen | CircuitBreaker의 timeout(ms) 기간을 설정 | (int) | 10 | Circuit Breaker |
bufferSizeInHalfOpen | CircuitBreaker가 Half-Open 상태일 때 허용 호출 수를 설정 (permittedNumberOfCallsInHalfOpenState) | (int) | 10 | Circuit Breaker |
failureRateThreshold | 실패 비율 임계값 백분율을 설정 실패 비율이 임계값보다 크거나 같을 때 CircuitBreaker은 open으로 전환하고 짧은 circuiting 호출을 시작 | (float) | 50 | Circuit Breaker |
'BACKEND' 카테고리의 다른 글
Hexagonal Architecture, 어렵지 않게 이해하기 (6) | 2023.08.03 |
---|---|
HTTP/3, 제대로 이해하기 (0) | 2023.01.01 |
Rate Limiter, 제대로 이해하기 (1) | 2022.11.20 |
Shenyu API Gateway, 어렵지 않게 시작하기 2 (0) | 2022.10.24 |
Shenyu API Gateway, 어렵지 않게 시작하기 1 (0) | 2022.10.23 |
Backend Software Engineer
𝐒𝐮𝐧 · 𝙂𝙮𝙚𝙤𝙣𝙜𝙨𝙪𝙣 𝙋𝙖𝙧𝙠