LLM WikiAccess-protected knowledge portal
← 스터디 홈
7편 · 약 20분

장애 감지와 복구 패턴: heartbeat, circuit breaker, bulkhead

장애는 ‘죽음’보다 ‘불확실성’에 가깝다

분산 시스템에서 가장 어려운 질문은 “저 노드가 죽었는가?”가 아니다. 더 정확한 질문은 “저 노드를 지금 신뢰하고 트래픽을 보내도 되는가?”다.

프로세스가 실제로 종료됐을 수도 있고, GC pause 때문에 몇 초 동안 멈췄을 수도 있다. 네트워크 파티션 때문에 한쪽에서는 응답이 없지만 다른 쪽에서는 정상으로 보일 수도 있다. 디스크 I/O가 포화되어 heartbeat는 늦지만 데이터는 아직 쓰고 있을 수도 있다.

그래서 장애 대응은 보통 네 단계로 나뉜다.

  1. 감지(detect): heartbeat, health check, timeout, error rate로 이상을 본다.
  2. 격리(isolate): readiness 제거, circuit breaker open, bulkhead 제한으로 전파를 막는다.
  3. 완화(mitigate): retry/backoff, failover, fallback, load shedding으로 사용자 영향을 줄인다.
  4. 복구(recover): half-open probe, replica rejoin, traffic ramp-up, 사후 검증으로 정상 상태로 돌린다.

핵심은 빠른 감지 하나가 아니라 오탐(false positive)과 지연 감지(false negative)의 균형이다. 너무 빨리 죽었다고 판단하면 정상 노드를 쫓아내 split-brain이나 불필요한 failover를 만들 수 있고, 너무 늦게 판단하면 죽은 노드에 계속 요청을 보내 장애를 키운다.


Heartbeat: 살아 있음을 주기적으로 증명하기

Heartbeat는 노드나 서비스가 주기적으로 “나는 아직 살아 있다”는 신호를 보내는 방식이다. 클러스터 멤버십, 리더 선출, replica health, workload scheduler에서 널리 쓰인다.

가장 단순한 방식은 고정 주기와 고정 timeout이다.

heartbeat interval = 1초
failure timeout = 5초
마지막 heartbeat 이후 5초가 지나면 suspect/down 처리

이 방식은 이해하기 쉽지만 운영에서는 두 가지 문제가 생긴다.

첫째, timeout이 너무 짧으면 네트워크 jitter나 일시적 CPU pause에도 노드를 죽었다고 판단한다. 둘째, timeout이 너무 길면 실제 장애를 늦게 감지해 클라이언트 요청이 계속 실패한다.

Monitor failure detector Node A service / replica heartbeat every 1s ack / observed timestamp t t+1 t+2 late timeout suspect/down 트래픽 차단 또는 failover
Heartbeat 기반 장애 감지의 기본 흐름

운영에서 heartbeat를 쓸 때는 다음을 분리해서 봐야 한다.

  • 프로세스 생존: 프로세스가 떠 있는가?
  • 서비스 준비: 요청을 처리할 준비가 되었는가?
  • 의존성 상태: DB, cache, downstream API가 사용 가능한가?
  • 데이터 안전성: primary가 쓰기를 받아도 되는 토폴로지인가?

이 네 가지를 하나의 /health로 합치면 위험하다. 예를 들어 데이터베이스가 잠시 느려졌다는 이유로 liveness가 실패해 모든 pod가 재시작되면, 원래의 DB 장애에 애플리케이션 crash loop가 더해진다.


고정 timeout의 한계와 φ accrual failure detector

고정 timeout은 단순하지만 환경 변화에 둔감하다. 평소 heartbeat 간격이 1초±50ms인 클러스터에서 3초 지연은 매우 수상하다. 반대로 WAN이나 overloaded node처럼 평소에도 1초±800ms로 흔들리는 환경에서는 3초 지연이 반드시 장애를 뜻하지 않는다.

φ accrual failure detector는 “살았는가/죽었는가”라는 boolean 대신 의심도(suspicion level)를 연속값으로 계산한다. 최근 heartbeat 도착 간격의 분포를 보고, 마지막 heartbeat 이후 시간이 길어질수록 φ 값을 높인다. 애플리케이션은 자신의 위험도에 맞춰 임계값을 정한다.

낮은 φ: 정상 지연 범위일 가능성이 큼
중간 φ: suspect, 새 요청을 줄이고 관찰 강화
높은 φ: down으로 간주, 트래픽 제외 또는 failover 후보

이 접근의 장점은 네트워크 상태가 흔들리는 환경에서 고정 timeout보다 유연하다는 점이다. 단점은 수학적 모델과 sampling window, 임계값을 이해하지 못하면 “왜 down 처리됐는지” 설명하기 어려워진다는 점이다.

운영자는 failure detector의 내부 값을 대시보드에 노출해야 한다. 단순히 node_down=1만 보면 원인 분석이 어렵다. 마지막 heartbeat 시각, heartbeat 지연 분포, φ 값, 멤버십 변경 이벤트, failover trigger를 함께 봐야 한다.


Kubernetes probe로 보는 health check 분리

Kubernetes는 장애 감지와 트래픽 제어를 명확히 나누기 위해 세 종류의 probe를 제공한다.

Probe묻는 질문실패 시 동작좋은 사용 예피해야 할 사용
startupProbe초기화가 끝났는가?성공 전까지 다른 probe 지연느린 부팅, migration 후 warm-up정상 운영 중 dependency 감시
livenessProbe프로세스가 회복 불가능하게 멈췄는가?컨테이너 재시작deadlock, stuck event loopDB 장애 때문에 실패 처리
readinessProbe지금 트래픽을 받아도 되는가?Service endpoint에서 제거DB 연결 불가, queue backlog 과다프로세스 재시작이 필요한 장애

DBA나 플랫폼 엔지니어 관점에서는 readinessProbe가 특히 중요하다. downstream DB가 overload 상태일 때 애플리케이션 pod를 죽이는 대신 트래픽을 잠시 빼야 하는 경우가 많다. 반대로 애플리케이션 내부 deadlock처럼 재시작 외에는 회복 방법이 없을 때만 liveness를 실패시켜야 한다.

좋은 probe endpoint는 가볍고 목적이 분명하다.

/livez   → 프로세스 내부 event loop, deadlock, critical goroutine 확인
/readyz  → traffic 처리 가능성: DB pool, queue depth, local cache warm-up 확인
/metrics → probe 판단에 쓰인 지표를 Prometheus/Grafana에서 확인

Circuit breaker: 실패한 의존성에 계속 매달리지 않기

Circuit breaker는 원격 호출 실패가 누적될 때 해당 의존성으로 가는 요청을 잠시 차단하는 패턴이다. 목적은 실패를 숨기는 것이 아니라, 실패한 dependency가 caller의 thread, connection, memory를 계속 점유하지 못하게 하는 것이다.

세 상태로 이해하면 쉽다.

  1. Closed: 정상 상태. 요청을 보낸다. 실패율과 latency를 측정한다.
  2. Open: 실패율이나 timeout이 임계값을 넘었다. 일정 시간 동안 즉시 실패(fail fast)하거나 fallback을 사용한다.
  3. Half-open: 회복 여부를 보기 위해 제한된 probe 요청만 허용한다. 성공하면 closed, 실패하면 다시 open.
Closed
정상 호출
실패율/timeout 증가 → Open
즉시 실패·fallback
cooldown 후 → Half-open
소량 probe
probe 성공: Closed로 복귀 probe 실패: Open 유지
Circuit breaker 상태 전이

Circuit breaker는 timeout과 별개다. timeout이 없으면 circuit breaker도 늦게 반응한다. 모든 remote call에는 먼저 deadline/timeout이 있어야 하고, 그 다음에 실패율 기반 breaker가 붙어야 한다.

또한 fallback은 공짜가 아니다. stale cache를 반환하거나 “일시적으로 사용할 수 없음”을 보여주는 것은 사용자 경험을 지키는 데 도움이 되지만, fallback 비율이 높아진 상태를 정상으로 착각하면 장애를 오래 방치한다. fallback count, open circuit count, half-open success rate는 반드시 alert 대상이 되어야 한다.


Retry, backoff, jitter: 회복을 돕거나 장애를 키우거나

Transient failure에는 retry가 효과적이다. 하지만 overload 장애에서는 retry가 추가 부하가 되어 복구를 늦춘다. AWS Builders Library가 표현하듯 retry는 “selfish”하다. 클라이언트 입장에서는 성공 가능성을 높이지만, 서버 입장에서는 이미 아픈 시스템에 요청을 더 얹는다.

안전한 retry의 조건은 다음과 같다.

  • 요청이 idempotent하거나 idempotency key가 있다.
  • retry 대상 error가 명확하다. 예: timeout, 429, 일부 5xx.
  • retry 횟수와 전체 deadline이 제한되어 있다.
  • exponential backoff를 쓰되 최대 대기시간을 cap으로 제한한다.
  • 모든 client가 동시에 재시도하지 않도록 jitter를 넣는다.
  • 여러 계층에서 중복 retry하지 않는다. 한 요청이 gateway, service A, service B에서 각각 3번 retry되면 부하가 곱셈으로 증가한다.

실무 판단 기준은 단순하다. “이 retry가 downstream 복구를 돕는가, 아니면 더 오래 아프게 만드는가?” overload, connection storm, DB lock contention에서는 retry보다 load shedding, queue 제한, circuit open이 먼저일 때가 많다.


Bulkhead: 장애의 폭발 반경을 줄이기

Bulkhead는 배의 격벽에서 온 비유다. 한 구역에 물이 들어와도 배 전체가 침몰하지 않도록 구획을 나누는 구조다. 소프트웨어에서는 thread pool, connection pool, queue, pod/resource limit, tenant partition을 분리해 한 dependency나 workload가 전체 자원을 먹지 못하게 한다.

예를 들어 API 서버가 다음 세 dependency를 호출한다고 하자.

  • 결제 API
  • 추천 API
  • 사용자 DB

모든 호출이 같은 thread pool과 HTTP connection pool을 공유하면 추천 API 장애가 결제와 로그인까지 멈출 수 있다. Bulkhead를 적용하면 dependency별 pool을 분리한다.

payment-client: max 50 concurrent calls
recommendation-client: max 20 concurrent calls
user-db-pool: max 80 connections

추천 API가 timeout으로 가득 차도 recommendation bulkhead 안에서만 포화된다. 결제와 로그인은 자신의 pool이 남아 있으면 계속 동작한다.

Bulkhead는 circuit breaker와 역할이 다르다.

패턴주 목적막는 장애
Timeout/deadline무한 대기 방지요청이 끝나지 않아 자원 점유
Circuit breaker실패 dependency로 가는 호출 차단cascade failure, retry storm
Bulkhead자원 격리한 dependency가 전체 thread/connection 고갈
Rate limit/load shedding과부하 진입 방지queue backlog, tail latency 폭증

좋은 운영 설계는 이들을 같이 쓴다. timeout 없는 breaker는 늦고, bulkhead 없는 breaker는 open 전까지 전체 자원이 고갈될 수 있으며, retry budget 없는 retry는 장애를 증폭한다.


Failover와 fencing: 복구가 데이터 손상을 만들지 않게 하기

장애 감지 후 가장 위험한 자동화는 primary 교체다. 단순히 heartbeat가 끊겼다는 이유만으로 새 primary를 세우면, 기존 primary가 네트워크 파티션 반대편에서 계속 쓰기를 받을 수 있다. 이것이 split-brain이다.

운영자는 failover 전에 fencing을 고려해야 한다. fencing은 old primary가 더 이상 쓰기를 받을 수 없도록 차단하는 절차다.

예시는 다음과 같다.

  • cloud API로 old primary instance stop 또는 network detach
  • storage lease 회수
  • VIP/Load Balancer target 제거
  • database superuser/password rotation 또는 write privilege revoke
  • consensus quorum을 통한 leader lease 만료 확인

데이터베이스, 메시지 큐, coordination service에서는 “빨리 새 primary를 만들기”보다 “두 primary가 동시에 쓰지 못하게 하기”가 더 중요하다. RTO를 줄이려다가 RPO를 무너뜨리면 복구가 아니라 데이터 손상 사고가 된다.


운영 대시보드와 알림 기준

장애 감지/복구 패턴은 코드보다 관측성이 중요하다. 다음 지표가 없으면 패턴이 실제로 안전하게 동작하는지 알 수 없다.

영역봐야 할 지표
Heartbeat마지막 heartbeat 시각, 지연 분포, missed count, membership change
Probeliveness/readiness 실패 횟수, restart count, endpoint removal duration
Timeoutdependency별 timeout rate, p95/p99 latency, deadline exceeded count
Circuit breakeropen/half-open/closed 상태, open duration, fallback rate, rejected call count
Bulkheadpool active/queued/rejected, connection pool saturation, queue wait time
Retryretry attempt count, retry success ratio, retry budget exhausted, jitter 적용 여부
Failoverleader change count, fencing 성공 여부, replica lag, post-failover consistency check

알림은 단일 지표보다 조합이 좋다.

좋지 않은 알림:
- circuit_open_count > 0

더 나은 알림:
- critical dependency circuit open 5분 이상
- fallback rate > 20% and request volume > baseline
- readiness removed pod 비율 > 30% and p99 latency 증가
- failover 발생 후 replica lag 감소하지 않음

실무 체크리스트

장애 감지와 복구를 설계할 때 다음 질문을 통과해야 한다.

  1. 각 remote call에 명시적 timeout/deadline이 있는가?
  2. retry는 idempotency와 retry budget을 갖고 있는가?
  3. liveness와 readiness가 분리되어 있는가?
  4. dependency 장애가 liveness 실패로 번역되어 crash loop를 만들지 않는가?
  5. circuit breaker의 open/half-open/closed 상태가 metric으로 보이는가?
  6. fallback이 실행될 때 사용자와 운영자가 모두 알 수 있는가?
  7. dependency별 thread/connection/queue bulkhead가 있는가?
  8. failover 전에 old primary를 fence할 수 있는가?
  9. 자동 복구 후 데이터 일관성, lag, backlog를 검증하는 절차가 있는가?
  10. 장애 훈련에서 timeout, breaker, bulkhead, failover가 실제로 작동하는지 확인했는가?

분산 시스템 운영의 목표는 “장애가 안 나게 하기”가 아니다. 장애는 난다. 중요한 것은 작은 장애가 전체 장애로 번지지 않게 하고, 자동 복구가 데이터 손상을 만들지 않게 하며, 복구 중 어떤 사용 경험을 제공할지 미리 정해두는 것이다.

References

  • Martin Fowler, “CircuitBreaker,” https://martinfowler.com/bliki/CircuitBreaker.html
  • Chris Richardson, “Pattern: Circuit Breaker,” https://microservices.io/patterns/reliability/circuit-breaker.html
  • Microsoft Azure Architecture Center, “Bulkhead pattern,” https://learn.microsoft.com/en-us/azure/architecture/patterns/bulkhead
  • AWS Builders Library, “Timeouts, retries, and backoff with jitter,” https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/
  • Marc Brooker, “Exponential Backoff And Jitter,” AWS Architecture Blog, https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
  • Kubernetes documentation, “Liveness, Readiness, and Startup Probes,” https://kubernetes.io/docs/concepts/configuration/liveness-readiness-startup-probes/
  • gRPC documentation, “Cancellation,” https://grpc.io/docs/guides/cancellation/
  • Microsoft Learn, “Reliable gRPC services with deadlines and cancellation,” https://learn.microsoft.com/en-us/aspnet/core/grpc/deadlines-cancellation
  • Naohiro Hayashibara, Xavier Défago, Rami Yared, Takuya Katayama, “The φ Accrual Failure Detector,” https://hdl.handle.net/10119/4784