콘텐츠로 이동

서킷 브레이커

Continuum Router에는 백엔드별 3상태 서킷 브레이커(Closed, Open, HalfOpen) 구현이 있으며 Admin API로 상태를 조회·조작할 수 있습니다. 일반 LLM 프록시 경로가 이를 구동하고 조회합니다. 백엔드를 선택한 뒤 전송 전에 허용 여부를 확인하고, 각 요청의 성공·실패 결과를 기록하며, 열린 서킷의 백엔드는 선택에서 제외해 트래픽이 우회하도록 합니다. 호환되는 모든 백엔드의 서킷이 열리면 표준 service-unavailable 오류를 반환하고 설정된 폴백에 참여합니다. 서킷 브레이커 보호는 능동 헬스 체크 및 재시도/폴백 라우팅을 대체하지 않고 보완합니다.

상태 머신

구현된 상태 머신의 동작은 다음과 같습니다.

상태 상태 머신 동작
closed 허용된 호출의 성공과 실패를 기록합니다. 연속 실패 임계값을 넘거나, minimum_requests 이후 실패율 임계값을 넘으면 열립니다.
open CircuitBreaker::allow_request를 통한 호출을 거부합니다. timeout 뒤 다음 허용 검사에서 half-open으로 전환합니다.
half_open 최대 half_open_max_requests개를 허용합니다. 연속 성공 횟수가 충족되면 닫히고, 실패하면 다시 열립니다.

이 의미는 서킷 브레이커 API를 호출하는 코드에 적용되며, 일반 프록시 요청 경로가 그 호출자입니다.

설정 참조

circuit_breaker:
  enabled: true
  failure_threshold: 5
  failure_rate_threshold: 0.5
  minimum_requests: 10
  timeout: 60s
  half_open_max_requests: 3
  half_open_success_threshold: 2
  failure_status_codes: [500, 502, 503, 504]
  timeout_as_failure: true
  sliding_window_size: 60s
  sliding_window_type: count_based  # count_based 또는 time_based

  backends:
    openai-primary:
      failure_threshold: 10
      failure_rate_threshold: 0.4
      timeout: 30s
      half_open_max_requests: 2
      half_open_success_threshold: 2

백엔드별 오버라이드는 backends 아래에 보인 다섯 필드만 지원합니다. minimum_requests, 실패 상태 코드, 타임아웃 처리, 슬라이딩 윈도 설정은 전역 값입니다.

기간 필드는 ms, s, m, h 같은 단위를 받습니다. 시작 전에 continuum-router config validate config.yaml로 검증하십시오.

Admin 엔드포인트

바이너리에 admin 기능이 포함되면 다음 인증 엔드포인트가 메인 서킷 브레이커 인스턴스를 조작합니다.

메서드 엔드포인트 용도
GET /admin/circuit/all 알려진 모든 상태와 상태별 개수 조회
GET /admin/circuit/{backend}/status 단일 백엔드 상태 조회. 유효하지만 모르는 이름을 조회하면 닫힌 기본 상태가 생성됨
POST /admin/circuit/{backend}/open 강제로 open 전환
POST /admin/circuit/{backend}/close 강제로 closed 전환
POST /admin/circuit/{backend}/reset 카운터와 상태 초기화

서킷 브레이커 설정이 없거나 비활성화되면 메인 인스턴스를 만들지 않습니다. 이때 변경 엔드포인트는 503 Service Unavailable을 반환하고 목록 엔드포인트는 "enabled": false를 보고합니다.

상태 응답 예:

{
  "backend": "openai-primary",
  "state": "closed",
  "failure_count": 0,
  "success_count": 0,
  "last_failure_time": null,
  "last_success_time": null,
  "last_transition_time": null,
  "next_retry_time": null,
  "consecutive_successes": 0,
  "half_open_requests": 0,
  "statistics": {
    "total_requests": 0,
    "failed_requests": 0,
    "successful_requests": 0,
    "success_rate": "0.00%",
    "times_opened": 0,
    "times_closed": 0,
    "average_open_duration_ms": 0.0
  }
}

기존 서킷 브레이커 설정의 변경은 설정 핫 리로드를 통해 생성된 상태 머신에 적용됩니다. 선택 구성 요소 자체의 활성화/비활성화는 재시작 시 변경으로 취급하십시오.

프록시 통합

데이터 평면은 실제로 백엔드에 도달한 전송마다 결과를 정확히 한 번 기록합니다.

  • 성공 응답은 성공으로 기록하며, half-open 상태에서는 서킷을 닫는 방향으로 진행시킵니다.
  • 설정된 실패 상태 코드(failure_status_codes, 기본값 500, 502, 503, 504)는 실패로 기록합니다. timeout_as_failure가 설정되면 타임아웃도 실패로 기록하고, 연결·전송 오류는 무조건 실패로 기록합니다.
  • failure_status_codes에 없는 상태 코드(기본 설정의 4xx와 429 포함)는 중립 결과로 기록합니다. 허용된 half-open 슬롯은 반납하지만 실패 카운트, 성공 카운트, 슬라이딩 윈도는 모두 그대로 둡니다. 업스트림 응답을 나타내지 않는 클라이언트/라우터 오류도 같은 방식으로 무시합니다.

허용 여부는 전송 전에 확인합니다. 아직 쿨다운 중인 열린 서킷이나 프로브 한도에 도달한 half-open 서킷은 요청을 거부하며, 라우터는 이를 재시도·폴백을 인지하는 백엔드 선택 결과로 처리해 다른 적격 백엔드나 폴백 모델을 시도합니다.

적용 범위를 정확히 적습니다. 있다고 적어 놓고 실제로는 없는 이음매가 아예 없는 것보다 나쁘기 때문입니다.

허용 판단과 결과 기록: Chat Completions, Completions, Embeddings, Rerank, 희소 임베딩, 표준 스트리밍, 제공자 네이티브 스트리밍(Anthropic, Gemini, Bedrock), Unix 소켓 스트리밍, 이미지 생성과 이미지 편집(Gemini 이미지 경로 포함), Responses 인그레스(/v1/responses의 변환 전략 네 가지와 패스스루·compact 포함), Chat Completions에서 Responses로 넘기는 브리지, Anthropic Messages 인그레스(네이티브, OpenAI 호환, Responses 기반, Bedrock Runtime, 웹 검색 에뮬레이션 경로), Anthropic count_tokens, 네이티브 Gemini 멀티모달 임베딩.

선택 단계 필터링은 위 경로에 백엔드를 공급하는 모든 선택 헬퍼에서 동작하므로, 백엔드를 고르기 전에 열린 서킷을 건너뛰고 건강한 동료를 선택합니다. 이 짝은 중요합니다. 허용 판단만 있으면 라운드로빈이 열린 서킷을 고를 때마다 처리 가능한 요청이 503으로 바뀝니다.

아직 적용되지 않은 곳: Responses 인그레스의 스트리밍 서비스(stream_service)와 그 패스스루 스트리밍 변형, 그리고 라우팅되지 않는 방어용 경로인 stream_with_auto_backend_selection.

스트리밍 결과의 해상도: 스트림 중간 폴백 릴레이는 종단 완료·스트림 실패·클라이언트 연결 해제까지 기록합니다. 그 밖의 스트리밍 경로는 핸드셰이크 상태만 기록하므로, 200으로 응답한 뒤 본문을 끊어버리는 백엔드는 성공으로 기록됩니다.

목록에 없는 상태 코드가 성공이 아니라 중립인 이유

브레이커가 세지 않도록 설정된 응답은 백엔드가 답을 했다는 증거이지 건강하다는 증거가 아닙니다. 이를 성공으로 기록하면 failure_count가 0으로 돌아가고 슬라이딩 윈도에 성공이 들어가므로, 429나 404를 진짜 5xx와 섞어 반환하는 백엔드는 임계치에 영영 도달하지 못했고 429만 반환하는 백엔드는 영원히 건강해 보였습니다. 중립 결과는 이 두 효과를 없애면서도 요청이 차지했던 half-open 프로브 슬롯은 그대로 반납합니다.

429는 기본 failure_status_codes에서 의도적으로 제외합니다. 일시적 rate limit은 백엔드가 살아 있고 스로틀링 중이라는 뜻이라, 서킷을 열면 쿨다운 내내 라우팅에서 빠지면서 스로틀링이 장애로 바뀝니다. 429는 이미 재시도 경로가 업스트림 Retry-After를 존중하며 처리하고, 비일시적 할당량·크레딧 소진에는 빠르게 실패합니다. 특정 백엔드의 429가 정말로 라우팅에서 빼야 한다는 뜻일 때만 failure_status_codes429를 추가하십시오.

half-open에서의 중립, 그리고 엔드포인트 간 초기화

HalfOpen은 중립 결과가 서킷을 움직이는 유일한 상태입니다. 이때 중립은 성공과 똑같이 half_open_success_threshold에 반영됩니다. HalfOpen이 던지는 질문은 Closed보다 좁습니다. "이 백엔드가 건강한가?"가 아니라 "이 백엔드로 가는 트래픽을 그만 막아도 되는가?"이고, 429나 404는 그 질문에 그렇다고 답합니다. 이 규칙이 없으면 전송은 복구됐지만 이제 429만 반환하는 백엔드가 HalfOpen에 영원히 머물면서(그 상태는 성공이나 실패로만 벗어납니다) half_open_max_requests개의 동시 요청으로 제한되고, 그 한도를 넘는 요청은 업스트림의 429와 Retry-After 대신 라우터의 503을 받게 됩니다. 종료 시한도 없습니다. Closed에서는 중립이 여전히 아무것도 기록하지 않으며, 이 비대칭이 이번 수정의 핵심입니다.

백엔드 하나가 여러 엔드포인트를 서비스할 때 알아둘 점이 하나 있습니다. 성공이 카운트를 0으로 되돌리기 때문에 failure_threshold연속 실패를 셉니다. 따라서 chat 엔드포인트는 실패하는데 image 엔드포인트는 정상인 백엔드는 절대 임계치에 도달하지 못할 수 있습니다. 사이사이 들어오는 image 성공이 카운트를 계속 초기화하기 때문입니다. 이 경우에도 failure_rate_threshold 경로는 연속 카운트가 아니라 슬라이딩 윈도를 보므로 여전히 서킷을 엽니다. 두 방아쇠가 모두 존재하고 둘 다 기본으로 켜져 있는 이유가 이것입니다. 트래픽이 섞인 백엔드에서 비율 경로가 더 빨리 반응하게 하려면 minimum_requests를 낮추십시오.

서킷 브레이커와 헬스 체크

두 장치는 서로 독립적이며 상호 보완합니다. 헬스 체커는 서킷 브레이커를 건드리지 않고, 브레이커의 Open에서 HalfOpen으로의 복구는 헬스 프로브가 아니라 자체 쿨다운과 실제 트래픽이 구동합니다.

헬스 체크 서킷 브레이커
신호 고정 주기의 대역 외 프로브 실제 요청의 대역 내 결과
죽은 백엔드 제외까지 걸리는 시간 health_checks.interval x health_checks.unhealthy_threshold (기본값 30s3 기준 90초) failure_threshold회 연속 실패, 부하 상황에서는 수 초
복구 healthy_threshold회 프로브 성공 쿨다운 후 실제 트래픽에 대한 제한된 half-open 프로브

두 열 사이의 간격이 운영상 핵심입니다. 브레이커가 없으면 제외가 전적으로 헬스 체크에 맡겨지므로, 최대 헬스 체크 창 전체 동안 모든 요청이 죽은 백엔드를 먼저 시도합니다. 폴백 체인이 설정되어 있으면 그 요청들은 모두 구제되어 아무것도 실패하지 않는데, 바로 그래서 이 상황을 놓치기 쉽습니다. 지속적인 부하에서는 요청마다 붙는 연결 시도가 백로그를 쌓아 첫 토큰 지연이 그 창 내내 회복되지 않고 저하된 채로 머뭅니다.

그래서 라우터는 fallback.fallback_chains가 설정되어 있는데 서킷 브레이커가 없으면 시작 시 경고를 남기며, 설정된 health_checks 값에서 노출 창을 계산해 알려줍니다.

WARN Fallback chains are configured but no circuit breaker is enabled. A backend that
     dies mid-traffic stays in the per-request rotation for up to 90s
     (health_checks.interval x health_checks.unhealthy_threshold) before health checking
     de-routes it. ...

브레이커는 계속 옵트인입니다. 기본으로 켜면 기존 배포 전체의 요청 라우팅이 바뀌기 때문입니다. 이 경고는 폴백 체인을 지워서가 아니라 circuit_breaker 섹션을 추가해서 없애십시오.

운영 지침

  • 일반 모델 선택에서 비정상 백엔드를 제외하려면 health_checks를 설정하십시오.
  • 일시적 실패와 모델 수준 대안에는 retryfallback을 설정하십시오.
  • Admin /admin/circuit/* 엔드포인트는 데이터 평면이 사용하는 것과 동일한 백엔드별 상태를 보고하므로 실제 트래픽으로 열린 서킷을 반영합니다.
  • 서킷 브레이커 Prometheus 메트릭(circuit_breaker_state, circuit_breaker_failures_total, circuit_breaker_successes_total, circuit_breaker_transitions_total)은 /metrics 레지스트리에 등록되며, metrics 기능과 서킷 브레이커가 모두 활성화되면 실제 프록시 트래픽을 반영합니다.

관련 문서