서킷 브레이커¶
라우터는 연쇄 장애를 방지하고 백엔드가 비정상 상태일 때 자동 장애 조치를 제공하기 위해 서킷 브레이커 패턴을 구현합니다.
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,
}
성능 고려 사항¶
서킷 브레이커는 핫 패스에서 최소한의 오버헤드를 위해 설계되었습니다:
- 원자적 연산: 모든 상태 확인은 락프리 원자적 연산 사용
- DashMap: 전역 락 없는 동시 해시맵
- 지연 초기화: 백엔드 상태는 첫 접근 시 생성
- 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"]
}
}
}
모범 사례¶
- 백엔드별 임계값 튜닝: 안정적인 백엔드는 높은 임계값, 불안정한 서비스는 낮은 임계값
- 상태 전환 모니터링: 잦은 개방/닫힘 주기에 대한 알림 설정 (회로 플래핑)
- 적절한 타임아웃 설정: 빠른 복구와 백엔드 과부하 사이의 균형
- 관리자 엔드포인트 신중하게 사용: 수동 오버라이드는 자동 보호를 우회
- 헬스 체크와 결합: 서킷 브레이커는 헬스 모니터링을 보완하지만 대체하지 않음