Skip to content

고가용성 스케줄링: 로테이션, 고정 계정, 스티키 세션 및 실패 재시도

Antigravity Tools를 로컬 AI 게이트웨이로 사용하다 보면, 곧바로 동일한 문제에 부딪히게 됩니다: 계정이 적을수록 429/401/invalid_grant가 자주 발생하고, 계정이 많을수록 "어떤 계정이 작업 중인지" 파악하기 어렵고, 캐시 적중률도 떨어집니다.

이 수업에서는 스케줄링 부분을 명확하게 설명합니다: 어떻게 계정을 선택하는지, "스티키 세션"이 무엇인지, 언제 강제 로테이션이 발생하는지, 그리고 "고정 계정 모드"를 사용하여 스케줄링을 제어할 수 있는 방법을 배웁니다.

수업을 마치면 할 수 있는 것

  • Antigravity Tools의 3가지 스케줄링 모드가 실제 요청에서 각각 무엇을 하는지 이해합니다
  • "세션 지문(session_id)"이 어떻게 생성되는지, 그리고 스티키 스케줄링에 어떤 영향을 미치는지 압니다
  • GUI에서 "고정 계정 모드"를 활성화/비활성화하고, 어떤 스케줄링 로직을 오버라이드하는지 이해합니다
  • 429/5xx/invalid_grant를 만났을 때, 시스템이 어떻게 제한을 표시하고 재시도하며 언제 로테이션하는지 예측할 수 있습니다

현재 겪고 있는 문제

  • Claude Code 또는 OpenAI SDK를 실행하다가 갑자기 429가 발생하고, 재시도하면 계정이 바뀌어 캐시 적중률이 떨어집니다
  • 여러 클라이언트가 동시에 작업을 실행하다가 서로의 계정 상태를 덮어쓰는 일이 자주 발생합니다
  • 문제 해결을 원하지만, 현재 요청이 어떤 계정이 서비스 중인지 모릅니다
  • "가장 안정적인 계정" 하나만 사용하고 싶지만, 시스템이 계속 로테이션합니다

언제 이 기능을 사용해야 하나요

  • "안정성(오류 감소)"과 "캐시 적중(동일 계정)" 사이의 균형을 맞춰야 할 때
  • 동일한 대화가 가능한 동일한 계정을 재사용하도록 하고 싶을 때(Prompt Caching 지터 감소)
  • 카나리/문제 해결을 위해 모든 요청을 하나의 계정에 고정하고 싶을 때

🎒 시작 전 준비

  1. 최소 2개의 사용 가능한 계정을 준비하세요(계정 풀이 작을수록 로테이션 공간이 작아집니다)
  2. 리버스 프록시 서비스가 시작되었는지 확인하세요("API Proxy" 페이지에서 Running 상태를 볼 수 있어야 합니다)
  3. 구성 파일의 위치를 알고 있습니다(수동으로 구성을 수정해야 하는 경우)

우선 구성 시스템 수업을 완료하세요

gui_config.json과 어떤 구성이 핫 업데이트 가능한지 아직 익숙하지 않다면, 먼저 **구성 완전 해설: AppConfig/ProxyConfig, 저장 위치 및 핫 업데이트 의미**를 보세요.

핵심 아이디어: 하나의 요청이 몇 단계의 "스케줄링"을 거치는가

스케줄링은 "단일 스위치"가 아니라, 여러 계층의 메커니즘이 결합된 것입니다:

  1. SessionManager가 먼저 요청에 세션 지문(session_id)을 할당합니다
  2. Handlers는 모든 재시도마다 TokenManager에게 강제 로테이션을 요청합니다(attempt > 0)
  3. TokenManager는 다음 우선순위로 계정을 선택합니다: 고정 계정 → 스티키 세션 → 60초 창 → 라운드 로빈
  4. 429/5xx를 만나면 제한 정보를 기록하고, 이후 계정 선택에서 제한된 계정을 건너뜁니다

"세션 지문(session_id)"이란 무엇인가?

세션 지문은 "가능한 한 안정적인 Session Key"로, 동일한 대화의 여러 요청을 동일한 계정에 바인딩하는 데 사용됩니다.

Claude 요청에서 우선순위는 다음과 같습니다:

  1. metadata.user_id(클라이언트가 명시적으로 전달하고, 비어있지 않으며 "session-" 접두사가 없는 경우)
  2. 첫 번째 "충분히 긴" user 메시지를 SHA256 해시한 후, sid-xxxxxxxxxxxxxxxx 형식으로 자릅니다

구현 위치: src-tauri/src/proxy/session_manager.rs(Claude/OpenAI/Gemini 각각의 추출 로직이 있음).

세부 사항: 왜 첫 번째 user 메시지만 보는가?

소스 코드에 명시적으로 "첫 번째 사용자 메시지 내용만 해시하고, 모델 이름이나 타임스탬프를 섞지 않는다"고 되어 있습니다. 목표는 동일한 대화의 여러 라운드 요청이 가능한 한 동일한 session_id를 생성하도록 하여 캐시 적중률을 높이는 것입니다.

TokenManager의 계정 선택 우선순위

TokenManager의 핵심 진입점은 다음과 같습니다:

  • TokenManager::get_token(quota_group, force_rotate, session_id, target_model)

수행하는 작업은 우선순위로 이해할 수 있습니다:

  1. 고정 계정 모드(Fixed Account): GUI에서 "고정 계정 모드"를 활성화했고(런타임 설정), 해당 계정이 제한되지 않고 할당량 보호도 없으면 바로 사용합니다.
  2. 스티키 세션(Session Binding): session_id가 있고 스케줄링 모드가 PerformanceFirst가 아니면, 해당 세션에 바인딩된 계정을 우선적으로 재사용합니다.
  3. 60초 전역 창 재사용: session_id를 전달하지 않았거나(또는 바인딩에 실패한 경우), PerformanceFirst가 아니면 가능한 한 60초 내에 "마지막으로 사용한 계정"을 재사용합니다.
  4. 라운드 로빈(Round-robin): 위의 경우가 해당되지 않으면, 전역 자동 증가 인덱스로 계정을 순차 선택합니다.

또한 두 가지 "보이지 않는 규칙"이 있어 사용 경험에 큰 영향을 미칩니다:

  • 계정은 먼저 정렬됩니다: ULTRA > PRO > FREE, 동일 티어 내에서는 남은 할당량이 높은 계정을 우선합니다
  • 실패하거나 제한된 계정은 건너뜁니다: 이미 시도하여 실패한 계정은 attempted 집합에 들어가고, 제한으로 표시된 계정은 건너뜁니다

3가지 스케줄링 모드의 차이점은 무엇인가

구성에서 다음을 볼 수 있습니다: CacheFirst / Balance / PerformanceFirst.

백엔드 TokenManager의 실제 분기를 기준으로, 핵심 차이는 하나뿐입니다: 스티키 세션 + 60초 창 재사용을 활성화할지 여부.

  • PerformanceFirst: 스티키 세션과 60초 창 재사용을 건너뛰고, 직접 라운드 로빈으로 갑니다(그리고 제한/할당량 보호 계정을 계속 건너뜁니다).
  • CacheFirst / Balance: 둘 다 스티키 세션과 60초 창 재사용을 활성화합니다.

max_wait_seconds에 대하여

프론트엔드/구성 구조에는 max_wait_seconds가 있고, UI는 CacheFirst에서만 조정을 허용합니다. 하지만 현재 백엔드 스케줄링 로직은 mode 분기만 기반으로 하며, max_wait_seconds를 읽지 않습니다.

실패 재시도와 "강제 로테이션"이 어떻게 연동되는가

OpenAI/Gemini/Claude의 handler에서 다음과 같은 패턴으로 재시도를 처리합니다:

  • 첫 번째 시도: force_rotate = false
  • 두 번째 이후: force_rotate = true(attempt > 0), TokenManager는 스티키 재사용을 건너뛰고 바로 다음 사용 가능한 계정을 찾습니다

429/529/503/500 등 오류를 만나면:

  • handler는 token_manager.mark_rate_limited(...)를 호출하여 이 계정을 "제한/과부하"로 기록하고, 이후 스케줄링에서 이를 건너뜁니다.
  • OpenAI 호환 경로는 오류 JSON에서 RetryInfo.retryDelay 또는 quotaResetDelay를 파싱하려 시도하고, 잠시 기다린 후 재시도를 계속합니다.

따라해보기: 스케줄링을 "제어 가능"하게 만들기

1단계: 먼저 정말 "계정 풀"이 있는지 확인하세요

이유 아무리 스케줄링이 고급이어도 풀에 계정이 1개뿐이면 선택할 여지가 없습니다. 많은 "로테이션이 작동하지 않음/스티키가 느껴지지 않음"의 근본 원인은 계정이 너무 적은 것입니다.

작업 "Accounts" 페이지를 열고, 최소 2개의 계정이 사용 가능한 상태인지 확인하세요(disabled/proxy disabled 아님).

볼 수 있는 것: 최소 2개의 계정이 정상적으로 할당량을 새로 고침할 수 있고, 리버스 프록시 시작 후 active_accounts가 0이 아닙니다.

2단계: GUI에서 스케줄링 모드 선택

이유 스케줄링 모드는 "동일한 대화"가 가능한 한 동일한 계정을 재사용할지, 아니면 매번 라운드 로빈할지를 결정합니다.

작업 "API Proxy" 페이지로 이동하여 "Account Scheduling & Rotation" 카드에서 모드 중 하나를 선택하세요:

  • Balance: 권장 기본값. 대부분의 경우 더 안정적입니다(세션 스티키 + 실패 시 로테이션).
  • PerformanceFirst: 동시성이 높고 작업이 짧으며, 처리량이 캐시보다 더 중요할 때 선택합니다.
  • CacheFirst: "대화를 가능한 한 동일한 계정에 고정"하고 싶다면 선택하세요(현재 백엔드와 Balance의 동작 차이는 거의 없습니다).

수동으로 구성을 수정하려면 다음과 같은 조각을 사용합니다:

json
{
  "proxy": {
    "scheduling": {
      "mode": "Balance",
      "max_wait_seconds": 60
    }
  }
}

볼 수 있는 것: 모드를 전환하면 즉시 gui_config.json에 기록되고, 리버스 프록시 서비스 런타임에 즉시 적용됩니다(재시작 불필요).

3단계: "고정 계정 모드" 활성화(로테이션 끄기)

이유 문제 해결, 카나리, 또는 특정 계정을 "특정 클라이언트에 고정"하고 싶을 때, 고정 계정 모드가 가장 직접적인 방법입니다.

작업 동일한 카드에서 "Fixed Account Mode"를 열고, 드롭다운에서 계정을 선택하세요.

기억하세요: 이 스위치는 리버스 프록시 서비스가 Running일 때만 사용할 수 있습니다.

볼 수 있는 것: 이후 요청은 모두 이 계정을 우선적으로 사용합니다. 계정이 제한되거나 할당량 보호에 걸리면, 라운드 로빈으로 폴백합니다.

고정 계정은 런타임 설정입니다

고정 계정 모드는 런타임 상태입니다(GUI 또는 API로 동적으로 설정). gui_config.json에 영구 저장되지 않습니다. 리버스 프록시 서비스를 재시작하면, 고정 계정이 비어 있어 복원됩니다(라운드 로빈 모드로 돌아감).

4단계: 필요할 때 "세션 바인딩" 지우기

이유 스티키 세션은 session_id -> account_id를 메모리에 기록합니다. 동일한 기계에서 다른 실험을 수행하는 경우(예: 계정 풀 전환, 모드 전환), 오래된 바인딩이 관찰을 방해할 수 있습니다.

작업 "Account Scheduling & Rotation" 카드 오른쪽 상단에서 "Clear bindings"를 클릭하세요.

볼 수 있는 것: 오래된 세션이 계정을 재할당합니다(다음 요청이 다시 바인딩됩니다).

5단계: 응답 헤더로 "어떤 계정이 서비스 중인지" 확인

이유 스케줄링이 예상대로 작동하는지 확인하려면, 서버가 반환하는 "현재 계정 식별자"를 얻는 것이 가장 확실한 방법입니다.

작업 OpenAI 호환 엔드포인트에 비스트림 요청을 보내고, 응답 헤더의 X-Account-Email을 관찰하세요.

bash
# 예: 최소한의 OpenAI Chat Completions 요청
# 주의: model은 현재 구성에서 사용 가능/라우팅 가능한 모델 이름이어야 합니다
curl -i "http://127.0.0.1:8045/v1/chat/completions" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk-REPLACE_ME" \
  -d '{
    "model": "gemini-3-pro-high",
    "stream": false,
    "messages": [{"role": "user", "content": "hello"}]
  }'

볼 수 있는 것: 응답 헤더에 다음과 유사한 내용이 나타납니다(예시):

text
X-Account-Email: [email protected]
X-Mapped-Model: gemini-3-pro-high

체크포인트 ✅

  • fixed account, sticky session, round-robin 세 가지 메커니즘이 서로 어떻게 오버라이드하는지 설명할 수 있습니다
  • session_id가 어디서 오는지 압니다(우선순위는 metadata.user_id, 없으면 첫 번째 user 메시지 해시)
  • 429/5xx를 만났을 때 시스템이 먼저 제한을 기록한 다음 계정을 바꾸어 재시도할 것이라 예측할 수 있습니다
  • X-Account-Email으로 현재 요청이 어느 계정이 서비스 중인지 확인할 수 있습니다

함정 경고

  1. 계정 풀에 계정이 1개만 있으면 "로테이션이 구원해줄 것"이라 기대하지 마세요 로테이션은 "다른 계정으로 바꾸는 것"입니다. 풀에 두 번째 계정이 없으면, 429/invalid_grant가 더 자주 노출됩니다.

  2. CacheFirst는 "사용 가능할 때까지 계속 기다리는 것"이 아닙니다 현재 백엔드 스케줄링 로직은 제한을 만나면 바인딩을 해제하고 계정을 전환하는 경향이 있으며, 장기간 차단 및 대기하지 않습니다.

  3. 고정 계정이 절대 강제는 아닙니다 고정 계정이 제한으로 표시되거나 할당량 보호에 적중되면, 시스템은 라운드 로빈으로 폴백합니다.

이 수업 요약

  • 스케줄링 체인: handler가 session_id 추출 → TokenManager::get_token으로 계정 선택 → 오류 시 attempt > 0 강제 로테이션
  • 가장 자주 사용하는 두 가지 스위치: 스케줄링 모드(스티키/60초 재사용 활성화 여부) + 고정 계정 모드(계정 직접 지정)
  • 429/5xx는 "제한 상태"로 기록되고, 이후 스케줄링에서 해당 계정을 건너뜁니다(잠금 만료까지)

다음 수업 예고

다음 수업에서는 모델 라우팅을 살펴봅니다: "안정적인 모델 컬렉션"을 외부에 노출하고 싶거나 와일드카드/프리셋 전략을 만들고 싶을 때, 어떻게 구성하고 문제 해결하는지.


부록: 소스 코드 참조

펼쳐서 소스 코드 위치 확인

업데이트 시간: 2026-01-23

기능파일 경로행 번호
스케줄링 모드 및 구성 구조(StickySessionConfig)src-tauri/src/proxy/sticky_config.rs1-36
세션 지문 생성(Claude/OpenAI/Gemini)src-tauri/src/proxy/session_manager.rs1-159
TokenManager: 고정 계정 모드 필드 및 초기화src-tauri/src/proxy/token_manager.rs27-50
TokenManager: 계정 선택 핵심 로직(고정 계정/스티키 세션/60초 창/라운드 로빈/할당량 보호)src-tauri/src/proxy/token_manager.rs470-940
TokenManager: invalid_grant 자동 비활성화 및 풀에서 제거src-tauri/src/proxy/token_manager.rs868-878
TokenManager: 제한 기록 및 성공 정리 APIsrc-tauri/src/proxy/token_manager.rs1087-1147
TokenManager: 스케줄링 구성 업데이트 / 세션 바인딩 정리 / 고정 계정 모드 settersrc-tauri/src/proxy/token_manager.rs1419-1461
ProxyConfig: scheduling 필드 정의 및 기본값src-tauri/src/proxy/config.rs174-257
리버스 프록시 시작 시 scheduling 구성 동기화src-tauri/src/commands/proxy.rs70-100
스케줄링 관련 Tauri 명령(get/update/clear bindings/fixed account)src-tauri/src/commands/proxy.rs478-551
OpenAI handler: session_id + 재시도 시 강제 로테이션src-tauri/src/proxy/handlers/openai.rs160-182
OpenAI handler: 429/5xx 제한 기록 + retry delay 파싱src-tauri/src/proxy/handlers/openai.rs349-367
Gemini handler: session_id + 재시도 시 강제 로테이션src-tauri/src/proxy/handlers/gemini.rs62-88
Gemini handler: 429/5xx 제한 기록 및 로테이션src-tauri/src/proxy/handlers/gemini.rs279-299
Claude handler: session_id 추출 및 TokenManager로 전달src-tauri/src/proxy/handlers/claude.rs517-524
429 retry delay 파싱(RetryInfo.retryDelay / quotaResetDelay)src-tauri/src/proxy/upstream/retry.rs37-66
제한 원인 식별 및 지수 백오프(RateLimitTracker)src-tauri/src/proxy/rate_limit.rs154-279

핵심 구조체:

  • StickySessionConfig: 스케줄링 모드 및 구성 구조(mode, max_wait_seconds)
  • TokenManager: 계정 풀, 세션 바인딩, 고정 계정 모드, 제한 추적기
  • SessionManager: 요청에서 session_id 추출