콘텐츠로 이동

서킷 브레이커

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개를 허용합니다. 연속 성공 횟수가 충족되면 닫히고 실패하면 다시 열립니다.

이 동작 규칙은 일반 프록시 데이터 평면, 프로바이더 네이티브 스트리밍 핸드셰이크, Admin 작업, 그리고 서킷 브레이커 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

  # 선택 사항: 성공했지만 느린 응답을 실패로 취급합니다 (이슈 #2964).
  latency_as_failure:
    ttft_ms: 5000              # 스트리밍: 프로바이더 디스패치부터 첫 바이트까지
    total_ms: 60000            # 버퍼링: 응답 전체 소요 시간
    minimum_samples: 10        # 위반이 집계되기 전 관측 표본 수
    model_overrides:
      gpt-5:
        ttft_ms: 15000         # 추론 모델은 첫 토큰 예산을 더 길게

  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, 실패 상태 코드, 타임아웃 처리, 슬라이딩 윈도 설정, latency_as_failure는 전역 값입니다. 모델별 지연 기준은 이 블록의 model_overrides 맵으로 조정합니다.

기간 필드는 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 허용 여부는 요청이 업스트림 전송으로 확정될 때만 확인합니다. 이 분리는 프리플라이트와 캐시 인지 경로에서 중요합니다. 선택된 백엔드에 호환성 검사, 캐시 키 확인, 가드레일 입력 게이트, 요청 변환을 수행할 때 half-open 프로브 슬롯을 소모하지 않기 때문입니다.

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

프록시 경로별 서킷 브레이커 적용 범위는 다음과 같습니다.

  • 전체 허용 판단 및 결과 기록:
    • Chat Completions, Completions, Embeddings, Rerank, 희소 임베딩.
    • 표준 스트리밍, 제공자 네이티브 스트리밍(Anthropic, Gemini, Bedrock), Unix 소켓 스트리밍.
    • 이미지 생성 및 이미지 편집(Gemini 이미지 경로 포함), 네이티브 Gemini 멀티모달 임베딩.
    • Responses 인그레스(/v1/responses의 변환 전략 네 가지와 패스스루·compact 포함), Chat Completions에서 Responses로 넘기는 브리지.
    • Anthropic Messages 인그레스(네이티브, OpenAI 호환, Responses 기반, Bedrock Runtime, 웹 검색 에뮬레이션 경로) 및 Anthropic count_tokens의 네이티브 Anthropic 경로.
  • 비프로브 허용 판단(전송 실패만 기록): 엔진 토크나이저 경로(/tokenize, /detokenize와 /v1 별칭)와 Anthropic count_tokens의 엔진 tokenize 경로.
    • 쿨다운 및 슬롯 동작: 쿨다운 중인 열린 서킷은 요청을 거부합니다. 쿨다운이 지나면 프로브와 마찬가지로 허용 판단이 서킷을 half-open으로 옮기며, half-open 서킷은 프로브 슬롯 점유 상태와 관계없이 슬롯을 차지하지 않고 요청을 허용합니다(Admission::open_non_probing, 내부 CircuitBreaker::allow_request_non_probing 및 선택 단계의 admits_selection_non_probing). 따라서 토크나이저 부하가 서킷을 닫을 생성 프로브를 지연시키지 않습니다.
    • HTTP 상태 중립 처리: 엔진이 반환한 HTTP 응답은 상태 코드와 관계없이 성공, 실패, half-open 진행 중 어느 것도 기록하지 않습니다. 토크나이저 응답은 생성 경로의 상태를 나타내지 않으며, 이 경로들에서는 count_tokens 텍스트를 포함해 클라이언트가 엔진의 5xx를 유발할 수 있기 때문입니다.
    • 전송 실패 기록: 연결 실패와 타임아웃만 실패로 기록합니다(record_non_probing_transport_failure, 일반 실패와 동일하게 집계하고 half-open 서킷을 다시 열지만 프로브 슬롯은 반환하지 않음). 타임아웃은 timeout_as_failure를 따릅니다. 서킷이 열린 뒤 토크나이저 트래픽만 유입되면 생성 요청이 결과를 기록할 때까지 half-open 상태로 남습니다.
    • count_tokens 분기 선택: count_tokens는 재선택 과정에서 다른 종류의 백엔드로 바뀔 수 있으므로 허용 판단 뒤에야 어느 분기를 탈지 알 수 있습니다. 후보에 엔진 토크나이저가 아닌 백엔드가 포함되면 프로브로 선택하고 tokenize 분기가 호출 전에 슬롯을 반환하며(Admission::into_non_probing), 모든 후보가 엔진 토크나이저이면 처음부터 비프로브 방식으로 선택하고 허용합니다.
  • 선택 단계 필터링과 전송 시점 허용 판단: 위 경로에 백엔드를 공급하는 모든 선택 헬퍼에서 두 단계가 함께 동작합니다. 허용 판단만 있으면 라운드로빈이 열린 서킷을 고를 때마다 처리 가능한 요청이 503으로 바뀌고, 선택 시점에 허용 슬롯을 잡으면 로컬 캐시 히트, 정책 거부, 가드레일 차단이 백엔드에 도달하지도 않은 채 희소한 half-open 프로브 슬롯을 점유하게 됩니다.
  • 미적용 경로: Responses 인그레스의 스트리밍 서비스(stream_service)와 그 패스스루 스트리밍 변형, 그리고 라우팅되지 않는 방어용 경로인 stream_with_auto_backend_selection.
  • 스트리밍 결과 기록 단위: 스트림 중간 폴백 릴레이는 종단 완료, 스트림 실패, 클라이언트 연결 해제까지 기록합니다. 그 밖의 스트리밍 경로는 핸드셰이크 상태만 기록하므로 200으로 응답한 뒤 본문을 끊는 백엔드는 성공으로 기록됩니다. 표준 OpenAI 와이어 chat 릴레이는 아래에서 설명하는 첫 바이트 지점에서 latency_as_failure를 추가로 적용하며, 다른 릴레이는 여전히 핸드셰이크에서 결과를 확정합니다.

지연을 실패 기준으로 사용하기

백엔드가 살아 있고 모든 요청에 2xx로 응답하더라도 첫 토큰이 클라이언트가 기다릴 수 있는 시간보다 오래 걸리면 쓸 수 없습니다. failure_threshold와 failure_rate_threshold는 이 경우를 보지 못합니다. 요청은 성공했고, 다만 너무 늦었을 뿐입니다. latency_as_failure는 느린 성공을 하나의 종단 실패로 집계하므로, 백엔드는 오류를 내는 백엔드와 같은 임계값과 쿨다운으로 라우팅에서 빠지고 같은 half-open 프로브로 복귀합니다.

latency_as_failure:
  ttft_ms: 5000
  total_ms: 60000
  minimum_samples: 10
  • ttft_ms는 스트리밍 경로의 한도입니다. 프로바이더 디스패치부터 첫 업스트림 바이트까지의 시간이며, 버퍼링 요청에는 적용되지 않습니다. 버퍼링 요청에는 응답 전체와 구분되는 첫 바이트가 없기 때문입니다.
  • total_ms는 버퍼링 경로의 한도입니다. 응답 전체 소요 시간이며, 스트리밍 요청에는 적용되지 않습니다.
  • 두 값 중 최소 하나는 설정해야 합니다. 둘 다 설정할 수 있으며, 각 관측은 자기 종류의 임계값만 사용합니다. 스트리밍 TTFT를 total_ms와 비교하는 일은 없습니다.
  • minimum_samples(기본값 10)는 서킷이 마지막으로 닫히거나 초기화되거나 슬라이딩 윈도가 비워진 뒤의 판정 표본 수로 정의되는 워밍업입니다. Closed에서는 처음 minimum_samples - 1개의 느린 관측을 성공으로 기록하고, 표본 수가 하한에 도달한 관측부터 실패로 집계합니다. half-open 프로브는 워밍업을 건너뜁니다. 서킷은 실제 증거로 열렸으므로 느린 프로브 결과는 즉시 다시 엽니다.
  • model_overrides는 서빙 모델 ID를 자체 ttft_ms, total_ms, minimum_samples에 매핑합니다. 오버라이드는 설정한 필드만 대체합니다.

판정 대상 시간은 클라이언트의 영향을 일부 받습니다. 큰 프롬프트는 첫 토큰 이전의 프리필 시간을 늘리고, 큰 max_tokens는 응답 전체 시간을 늘립니다. 신뢰할 수 없는 클라이언트를 받는 환경에서는 minimum_samples와 임계값을 높이거나 model_overrides로 모델별 기준을 두어, 비싼 요청을 성능이 저하된 백엔드로 오인하지 않도록 하십시오.

각 디스패치는 정확히 하나의 결과만 냅니다. 성공 뒤에 실패가 이어지는 일은 없습니다. 판정을 기록 전에 계산하므로 느린 결과는 record_success로 실패 카운트를 먼저 초기화하지 않고 바로 실패 카운트를 올립니다. 빠른 결과는 평범한 성공으로 기록합니다. 전환 로그와 circuit_breaker_transitions_total의 reason 레이블은 slow를 보고하고, 느린 결과는 circuit_breaker_failures_total{error_type="slow"}로 집계됩니다.

적용 범위는 표준 OpenAI 와이어 chat 경로입니다. 버퍼링 응답은 본문 전체를 읽은 시점에 판정하고, 스트리밍 응답은 릴레이의 첫 바이트 기록 시점으로 핸드셰이크 결과 판정을 미룹니다. 지연된 스트림이 첫 바이트 없이 끝나면 허용은 성공도 실패도 아닌 중립으로 반납됩니다.

다음 경로에는 아직 지연 기준을 적용하지 않습니다.

  • Responses 인그레스 스트리밍 서비스와 그 패스스루 변형.
  • 프리스트림·미드스트림 폴백 릴레이.
  • 프로바이더 네이티브 스트리밍 핸드셰이크(Anthropic, Gemini, Bedrock).
  • Unix 소켓 스트리밍.
  • 이미지·임베딩·리랭크·토크나이저 경로.

LeastLatency 선택과 이 기준은 서로 보완합니다. LeastLatency는 새 요청을 관측상 가장 빠른 백엔드로 유도할 뿐 백엔드를 제거하지 않으며, latency_as_failure는 운영자가 정한 한도를 넘은 백엔드를 제거합니다.

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

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

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

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

HalfOpen은 중립 결과가 서킷을 움직이는 유일한 상태이지만, 중립 증거만으로 서킷을 닫지는 않습니다. 중립 프로브는 half-open 진행 횟수에 반영되므로 전송은 복구됐지만 429만 반환하는 백엔드가 종료 시한 없이 half_open_max_requests개의 동시 요청 한도에 묶여 HalfOpen에 머물지 않습니다. half_open_success_threshold에 도달했을 때 그 임계치 창 안에 실제 성공 프로브가 최소 하나라도 있어야만 브레이커가 닫힙니다. 창 전체가 중립 결과뿐이면 서킷은 새 쿨다운과 함께 다시 Open으로 돌아갑니다. Closed에서는 중립 결과가 아무것도 기록하지 않습니다.

단일 백엔드가 여러 엔드포인트를 서비스할 때 failure_threshold는 성공 시 카운트가 0으로 초기화되므로 연속 실패를 집계합니다. 따라서 chat 엔드포인트는 실패하는데 image 엔드포인트는 정상인 백엔드는 중간에 끼어드는 image 성공이 카운트를 계속 초기화해 절대 임계치에 도달하지 못할 수 있습니다. 이 경우에도 failure_rate_threshold 경로는 연속 카운트 대신 슬라이딩 윈도를 기준으로 서킷을 엽니다. 두 조건이 모두 존재하고 기본으로 활성화되어 있는 이유가 여기에 있으며, 혼합 트래픽 백엔드에서 비율 경로가 더 빨리 반응하게 하려면 minimum_requests를 낮추십시오.

서킷 브레이커와 헬스 체크

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

헬스 체크 서킷 브레이커
신호 고정 주기의 대역 외 프로브 실제 요청의 대역 내 결과
무응답 백엔드 제외까지 걸리는 시간 health_checks.interval x health_checks.unhealthy_threshold (기본값 30s와 3 기준 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를 설정하십시오.
  • 일시적 실패와 모델 수준 대안에는 retry와 fallback을 설정하십시오.
  • Admin /admin/circuit/* 엔드포인트는 데이터 평면이 사용하는 것과 동일한 백엔드별 상태를 보고하므로 실제 트래픽으로 열린 서킷을 반영합니다.
  • 서킷 브레이커 Prometheus 메트릭(circuit_breaker_state, circuit_breaker_failures_total, circuit_breaker_successes_total, circuit_breaker_transitions_total)은 /metrics 레지스트리에 등록되며 metrics 기능과 서킷 브레이커가 모두 활성화되면 실제 프록시 트래픽을 반영합니다.

관련 문서