콘텐츠로 이동

서킷 브레이커

라우터는 연쇄 장애를 방지하고 백엔드가 비정상 상태일 때 자동 장애 조치를 제공하기 위해 서킷 브레이커 패턴을 구현합니다.

3상태 머신

상태 설명

상태 동작 전환 트리거
Closed 정상 작동. 모든 요청 통과. 실패 카운트. failure_threshold 초과 또는 failure_rate_threshold 초과 시 (minimum_requests 충족) Open으로 전환
Open 빠른 실패 모드. 모든 요청 503 Service Unavailable로 즉시 거부. timeout_seconds 만료 후 HalfOpen으로 전환
HalfOpen 복구 테스트. 제한된 요청 허용 (half_open_max_requests). half_open_success_threshold 성공 후 Closed로 전환. 실패 시 재개방.

설정

circuit_breaker:
  enabled: true

  # 장애 감지
  failure_threshold: 5          # 개방까지의 연속 실패 횟수
  failure_rate_threshold: 0.5   # 50% 실패율 임계값
  minimum_requests: 10          # 비율 계산 전 최소 요청 수

  # 타이밍
  timeout_seconds: 60           # 회로가 열려있는 시간
  half_open_max_requests: 3     # 반개방 상태의 최대 동시 요청
  half_open_success_threshold: 2 # 닫힘에 필요한 성공 횟수

  # 실패로 간주되는 상태 코드
  failure_status_codes:
        - 500
        - 502
        - 503
        - 504

  # 백엔드별 오버라이드
  backends:
    openai-primary:
      failure_threshold: 10     # 안정적인 백엔드에 더 관대
      timeout_seconds: 30       # 더 빠른 복구 시도
    local-llm:
      failure_threshold: 3      # 로컬 서비스에 덜 관대
      timeout_seconds: 120      # 복구 전 더 긴 대기

백엔드별 격리

각 백엔드는 독립적인 서킷 브레이커 상태를 유지합니다:

pub struct CircuitBreaker {
    states: Arc<DashMap<String, BackendCircuitState>>,
    config: CircuitBreakerConfig,
}

// 각 백엔드는 독립적인 상태를 가짐
pub struct BackendCircuitState {
    state: AtomicU8,                  // 0=Closed, 1=Open, 2=HalfOpen
    failure_count: AtomicU32,
    success_count: AtomicU32,
    total_requests: AtomicU64,
    last_failure_time: AtomicU64,
    last_state_change: AtomicU64,
    half_open_requests: AtomicU32,    // 반개방 상태의 현재 요청 수
    consecutive_successes: AtomicU32,
}

관리자 엔드포인트

서킷 브레이커의 수동 제어는 관리자 엔드포인트를 통해 가능합니다:

엔드포인트 메서드 설명
/admin/circuit/all GET 모든 서킷 브레이커 상태 목록
/admin/circuit/:backend/status GET 특정 백엔드 상태 조회
/admin/circuit/:backend/open POST 회로 강제 개방
/admin/circuit/:backend/close POST 회로 강제 닫기
/admin/circuit/:backend/reset POST 회로 초기 상태로 리셋

응답 예제 (GET /admin/circuit/openai-primary/status):

{
  "backend": "openai-primary",
  "state": "closed",
  "failure_count": 2,
  "success_count": 1547,
  "total_requests": 1549,
  "failure_rate": 0.0013,
  "last_failure_time": "2024-01-15T10:30:00Z",
  "last_state_change": "2024-01-15T08:00:00Z",
  "half_open_requests": 0,
  "consecutive_successes": 1547
}

Prometheus 메트릭

# 각 회로의 현재 상태 (0=Closed, 1=Open, 2=HalfOpen)
circuit_breaker_state{backend="openai-primary"} 0

# 총 상태 전환
circuit_breaker_transitions_total{backend="openai-primary", from="closed", to="open"} 3
circuit_breaker_transitions_total{backend="openai-primary", from="open", to="half_open"} 3
circuit_breaker_transitions_total{backend="openai-primary", from="half_open", to="closed"} 3

# 기록된 결과
circuit_breaker_successes_total{backend="openai-primary"} 15470
circuit_breaker_failures_total{backend="openai-primary"} 12

폴백 전략

회로가 열리면 시스템은 다른 폴백 전략을 적용할 수 있습니다:

pub enum FallbackStrategy {
    /// 동일 모델을 지원하는 다음 사용 가능한 백엔드 사용
    NextAvailable,
    /// 특정 백업 백엔드 사용
    SpecificBackend(String),
    /// 가능한 경우 캐시된 응답 반환
    CachedResponse,
    /// 저하된 서비스 사용 (예: 더 작은/빠른 모델)
    DegradedService(String),
    /// 즉시 오류로 실패
    FailFast,
}

성능 고려 사항

서킷 브레이커는 핫 패스에서 최소한의 오버헤드를 위해 설계되었습니다:

  1. 원자적 연산: 모든 상태 확인은 락프리 원자적 연산 사용
  2. DashMap: 전역 락 없는 동시 해시맵
  3. 지연 초기화: 백엔드 상태는 첫 접근 시 생성
  4. HalfOpen용 CAS 루프: 요청 제한에서 경쟁 조건 방지
// 핫 패스 - 회로가 열렸는지 확인 (락프리)
pub fn is_open(&self, backend: &str) -> bool {
    if let Some(state) = self.states.get(backend) {
        matches!(state.get_state(), CircuitState::Open)
    } else {
        false  // 상태 없음 = 회로 닫힘
    }
}

오류 응답 형식

회로가 열려서 요청이 거부될 때:

{
  "error": {
    "message": "서킷 브레이커로 인해 서비스를 일시적으로 사용할 수 없습니다",
    "type": "circuit_breaker_open",
    "code": 503,
    "details": {
      "backend": "openai-primary",
      "circuit_state": "open",
      "retry_after": 60,
      "alternative_backends": ["openai-secondary", "azure-openai"]
    }
  }
}

모범 사례

  1. 백엔드별 임계값 튜닝: 안정적인 백엔드는 높은 임계값, 불안정한 서비스는 낮은 임계값
  2. 상태 전환 모니터링: 잦은 개방/닫힘 주기에 대한 알림 설정 (회로 플래핑)
  3. 적절한 타임아웃 설정: 빠른 복구와 백엔드 과부하 사이의 균형
  4. 관리자 엔드포인트 신중하게 사용: 수동 오버라이드는 자동 보호를 우회
  5. 헬스 체크와 결합: 서킷 브레이커는 헬스 모니터링을 보완하지만 대체하지 않음

관련 문서