LabHub
배우기 러닝패스 코스

Microservice Architecture

The Circuit Breaker's State Machine

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

서킷 브레이커는 장애를 고치지 않는다. 장애가 확실할 때 빠르게 실패해서, 호출자의 자원이 마르지 않게 지킨다.

Concept map: 타임아웃. · 호출 사슬의 안쪽으로 갈수록 짧아야 한다. · 격벽(bulkhead). · 장애가 번지는 경로의 대부분이 이 공유 자원입니다.

왜 이게 필요했나

재시도만으로는 부족합니다. 다운스트림이 정말로 죽어 있을 때 재시도는 상황을 악화시키기만 합니다. 그리고 호출자 입장에서도, 어차피 실패할 호출을 3초씩 기다리는 것은 커넥션과 스레드의 낭비입니다.

서킷 브레이커는 "최근 결과를 보니 이 서비스는 지금 안 된다"고 판단하면 호출을 아예 시도하지 않고 즉시 실패를 돌려줍니다. 마이크로초 단위로 실패하므로 호출자의 자원은 마르지 않고, 다운스트림은 회복할 숨을 얻습니다.

어떻게 동작하나

세 상태입니다.

서킷 브레이커의 세 상태와 네 전이 — CLOSED 는 전부 통과시키며 결과를 윈도우에 기록하고, 실패율이 임계값을 넘으면 OPEN 으로 가 즉시 거부하고, waitDurationInOpenState 가 지나면 HALF_OPEN 으로 가 시험 호출만 허용하며, 그 결과로 CLOSED 로 돌아가거나 다시 OPEN 이 된다. 4xx 는 실패로 세지 않는다

CLOSED 는 정상입니다. 모든 호출이 통과하고, 결과를 슬라이딩 윈도우에 기록합니다. 윈도우 안의 실패율이 임계값을 넘으면 OPEN 으로 갑니다.

OPEN 은 차단입니다. 모든 호출이 즉시 거부됩니다. waitDurationInOpenState 가 지나면 HALF_OPEN 으로 갑니다.

HALF_OPEN 은 시험입니다. 정해진 수의 시험 호출만 허용하고, 그 결과로 CLOSED 로 복귀하거나 OPEN 으로 돌아갑니다.

설정값에는 정답이 없지만 출발점은 있습니다. 저자가 운영에서 쓰는 값은 이렇습니다. slidingWindowSize: 10, minimumNumberOfCalls: 5, failureRateThreshold: 50, slowCallDurationThreshold: 3s, waitDurationInOpenState: 30s, permittedNumberOfCallsInHalfOpenState: 3. 서비스 성격에 따라 임계값을 조정합니다 — 결제처럼 실패가 치명적인 곳은 30–40%, 알림처럼 관대해도 되는 곳은 60–70% 입니다.

여기서 가장 중요한 설계 결정 하나. 무엇을 실패로 셀 것인가입니다. 4xx 는 실패가 아닙니다. 클라이언트가 잘못 보낸 것이므로 서킷이 개입할 사안이 아닙니다. 실제 사고 사례가 있습니다. 재고 서비스가 특정 상품에 400 을 돌려주기 시작했는데, 그 400 이 실패율에 집계되면서 서킷이 OPEN 이 됐고, 정상 상품 조회까지 전부 차단됐습니다. 서킷은 5xx, 타임아웃, 연결 실패에만 반응해야 합니다.

현장에서 만나는 모습

두 번째로 흔한 사고는 HALF_OPEN 플래핑입니다. permittedNumberOfCallsInHalfOpenState 를 1 로 두면 시험 호출 한 건이 우연히 실패할 때마다 다시 OPEN 으로 떨어집니다. 다운스트림이 간헐적으로만 응답하는 상황에서 영영 CLOSED 로 못 돌아옵니다. 최소 3, 보통 5~10 을 씁니다.

세 번째. minimumNumberOfCalls 의 기본값이 100 인 구현이 많습니다. 호출 빈도가 낮은 서비스에서는 윈도우가 채워지기 전에 장애가 끝나 버려서 서킷이 한 번도 안 열립니다. "서킷을 넣었는데 안 열려요"의 절반은 이 이유입니다.

그리고 서킷은 반드시 폴백과 함께 씁니다. 폴백이 return null 이면 서킷은 장애를 NullPointerException 으로 바꾼 것에 불과합니다. 캐시된 값, 축소된 응답, 또는 명확한 안내 중 하나여야 합니다.

서킷 브레이커만으로는 부족하다

서킷은 이미 나빠진 뒤에 반응하는 장치다. 그 앞뒤로 함께 두어야 할 것들이 있고, 넷이 모여야 하나의 방어선이 된다.

타임아웃. 가장 기본이면서 가장 자주 빠뜨린다. 타임아웃이 없으면 서킷이 실패를 셀 기회조차 없이 호출자의 스레드가 무한정 묶인다. 값은 상대의 지연 분포를 보고 정하되, 호출 사슬의 안쪽으로 갈수록 짧아야 한다. 바깥이 3초인데 안쪽이 5초면 안쪽이 답하기 전에 바깥이 이미 포기해서, 안쪽의 작업이 통째로 낭비된다.

격벽(bulkhead). 한 상대에게 쓸 수 있는 동시 호출 수를 따로 제한한다. 이것이 없으면 느려진 서비스 하나가 공용 스레드 풀이나 커넥션 풀을 전부 차지해, 그 서비스와 무관한 기능까지 함께 멈춘다. 장애가 번지는 경로의 대부분이 이 공유 자원입니다.

재시도. 서킷과 반대 방향으로 작동하므로 함께 쓸 때 규칙이 필요하다. 멱등한 호출에만, 상한을 두고, 간격에 무작위를 섞어서. 그리고 사슬의 여러 층에서 각자 재시도하면 곱해진다. 세 층이 각각 세 번씩 재시도하면 최악의 경우 27번이다. 재시도는 사슬에서 한 층만 맡는 것이 원칙이다.

폴백. 서킷이 열렸을 때 무엇을 돌려줄지다. 앞에서 말했듯 예외를 다시 던지는 폴백은 아무것도 아니다. 좋은 폴백은 대개 셋 중 하나다. 조금 낡은 캐시 값, 그 부분만 뺀 축소된 응답, 또는 "지금은 이 정보를 못 가져왔다" 는 것을 화면이 알아들을 수 있는 형태로 알리는 것.

마지막으로 이 넷은 시험되어야 한다. 평소에 한 번도 열려 본 적 없는 서킷은 진짜 장애 때 처음 동작하고, 그때 폴백에 결함이 있으면 방어선이 오히려 장애를 만든다. 상대를 일부러 죽여 보는 훈련을 정기적으로 하는 이유가 이것이다.

다음 실습에서 할 것

서킷 브레이커를 직접 구현합니다. 상태 전이를 눈으로 보고, OPEN 에서 다운스트림에 호출이 정말 가지 않는지 카운터로 증명하고, 4xx 를 실패로 세지 않게 만들고, 마지막에 전체 전이 이력을 파일로 남깁니다.