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: example@gmail.com
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 추출