高可用性スケジューリング:ローテーション、固定アカウント、スティッキーセッションと失敗時再試行
Antigravity Tools をローカル AI ゲートウェイとしてしばらく使っていると、必ず同じ問題にぶつかります:アカウントが少ないほど 429/401/invalid_grant が発生しやすく、アカウントが多いほど「どのアカウントが動いているか」が不明瞭になり、キャッシュヒット率も低下します。
このレッスンでは、スケジューリングについて明確に解説します:アカウントをどう選択するか、「スティッキーセッション」とは何か、いつ強制的にローテーションするか、そして「固定アカウントモード」を使ってスケジューリングを制御可能にする方法。
学んだ後できること
- Antigravity Tools の 3 種類のスケジューリングモードが実際のリクエストで何をするかを理解できる
- 「セッションフィンガープリント(session_id)」をどう生成するか、そしてそれがスティッキースケジューリングにどう影響するかを理解できる
- GUI で「固定アカウントモード」を有効/無効にし、どのスケジューリングロジックを上書きするかを理解できる
- 429/5xx/invalid_grint に遭遇したとき、システムがどうレート制限をマークし、どう再試行し、いつローテーションするかを理解できる
現在の悩み
- Claude Code または OpenAI SDK を実行していると突然 429 になり、再試行するとアカウントが切り替わり、キャッシュヒット率が低下する
- 複数のクライアントが同時にタスクを実行し、互いのアカウント状態を「上書き」することがよくある
- トラブルシューティングしたいが、現在のリクエストがどのアカウントで処理されているかわからない
- 「最も安定したアカウント」だけを使いたいが、システムが常にローテーションしている
この方法をいつ使うか
- 「安定性(エラーを減らすこと)」と「キャッシュヒット(同じアカウント)」のトレードオフが必要な場合
- 同じ会話をできるだけ同じアカウントで再利用したい場合(Prompt Caching の揺らぎを減らす)
- カナリアリリース/トラブルシューティングを行い、すべてのリクエストを特定のアカウントに固定したい場合
🎒 始める前の準備
- 少なくとも 2 つの利用可能なアカウントを準備してください(アカウントプールが小さいほど、ローテーションの余地が小さくなります)
- リバースプロキシサービスが起動していること(「API Proxy」ページで Running 状態が表示されていること)
- 設定ファイルの場所を知っていること(手動で設定を変更する必要がある場合)
まず設定システムのレッスンを補完してください
gui_config.json とどの設定がホットアップデート可能かをまだよく知らない場合は、先に 設定完全解説:AppConfig/ProxyConfig、永続化場所とホットアップデートの意味 を参照してください。
コアコンセプト:一回のリクエストがどのような「スケジューリング」層を通過するか
スケジューリングは「単一のスイッチ」ではなく、いくつかのメカニズムが重なっています:
- SessionManager が最初にリクエストにセッションフィンガープリント(session_id)を割り当てます
- Handlers は再試行するたびに TokenManager に強制的ローテーションを要求します(
attempt > 0) - TokenManager は次の順序でアカウントを選択します:固定アカウント → スティッキーセッション → 60秒ウィンドウ → ラウンドロビン
- 429/5xx に遭遇するとレート制限情報を記録し、後続のアカウント選択でレート制限されたアカウントを積極的にスキップします
「セッションフィンガープリント(session_id)」とは?
セッションフィンガープリントは「できるだけ安定した Session Key」で、同じ会話の複数のリクエストを同じアカウントにバインドするために使用されます。
Claude リクエストでは、優先順位は次の通りです:
metadata.user_id(クライアントが明示的に渡し、かつ空ではなく、かつ"session-"プレフィックスを含まないもの)- 最初の「十分に長い」ユーザーメッセージを SHA256 ハッシュして、
sid-xxxxxxxxxxxxxxxxに切り詰めたもの
対応する実装:src-tauri/src/proxy/session_manager.rs(Claude/OpenAI/Gemini にはそれぞれの抽出ロジックがあります)。
小さな詳細:なぜ最初のユーザーメッセージだけを見るのか?
ソースコードには「最初のユーザーメッセージのみをハッシュし、モデル名やタイムスタンプを混ぜない」と明記されています。これは、同じ会話の複数のラウンドリクエストができるだけ同じ session_id を生成し、キャッシュヒット率を向上させることを目的としています。
TokenManager のアカウント選択優先順位
TokenManager のコアエントリーポイントは:
TokenManager::get_token(quota_group, force_rotate, session_id, target_model)
その処理は優先順位で理解できます:
- 固定アカウントモード(Fixed Account):GUI で「固定アカウントモード」が有効になっている(ランタイム設定)場合、そのアカウントがレート制限されておらず、クォータ保護にも設定されていない場合、直接それを使用します。
- スティッキーセッション(Session Binding):
session_idがあり、スケジューリングモードがPerformanceFirstでない場合、そのセッションにバインドされたアカウントを優先的に再利用します。 - 60秒グローバルウィンドウ再利用:
session_idが渡されていない(またはバインドに成功していない)場合、PerformanceFirst以外では 60 秒以内に「最後に使用したアカウント」をできるだけ再利用します。 - ラウンドロビン(Round-robin):上記が適用できない場合、グローバル自動インデックスに従ってアカウントをラウンドロビンで選択します。
さらに 2 つの「見えないルール」があり、ユーザー体験に大きく影響します:
- アカウントは最初にソートされます:ULTRA > PRO > FREE、同じティア内では残りクォータが高いアカウントが優先されます。
- 失敗またはレート制限はスキップされます:失敗したアカウントは
attempted集合に入ります。レート制限とマークされたアカウントはスキップされます。
3 種類のスケジューリングモードの違いは何か
設定では、次のものが表示されます:CacheFirst / Balance / PerformanceFirst。
「バックエンド TokenManager の実際の分岐」を基準にすると、重要な違いは 1 つだけです:スティッキーセッションと 60秒ウィンドウ再利用を有効にするかどうか。
PerformanceFirst:スティッキーセッションと 60秒ウィンドウ再利用をスキップし、直接ラウンドロビンを行います(レート制限/クォータ保護されたアカウントも引き続きスキップ)。CacheFirst/Balance:どちらもスティッキーセッションと 60秒ウィンドウ再利用を有効にします。
max_wait_seconds について
フロントエンド/設定構造には max_wait_seconds があり、UI では CacheFirst のみ調整可能です。しかし、現在のバックエンドスケジューリングロジックは mode の分岐に基づいており、max_wait_seconds を読み取っていません。
失敗時再試行と「強制的ローテーション」はどう連動するか
OpenAI/Gemini/Claude の handler では、次のようなパターンで再試行を処理します:
- 1 回目の試行:
force_rotate = false - 2 回目以降:
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」カードを見つけて、次のモードの 1 つを選択してください:
Balance:推奨デフォルト値。ほとんどの場合より安定しています(セッションスティッキー + 失敗時ローテーション)。PerformanceFirst:同時実行性が高く、タスクが短く、キャッシュよりスループットを重視する場合に選択します。CacheFirst:「会話をできるだけ固定アカウント」にしたい場合、これを選択できます(現在のバックエンドはBalanceと動作の違いがほとんどありません)。
手動で設定を変更する場合、対応するフラグメントは:
{
"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 を観察してください。
# 例:最小限の 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"}]
}'確認すべきもの:レスポンスヘッダーに次のような内容が表示されます(例):
X-Account-Email: [email protected]
X-Mapped-Model: gemini-3-pro-highチェックポイント ✅
fixed account、sticky session、round-robinの 3 つのメカニズムのどれがどれを上書きするかを明確に説明できるsession_idがどうやって来るかを知っている(優先metadata.user_id、それ以外は最初の user メッセージをハッシュ)- 429/5xx に遭遇したとき、システムが先にレート制限を記録し、アカウントを切り替えて再試行することを予測できる
X-Account-Emailを使って現在のリクエストがどのアカウントでサービスを提供しているかを確認できる
よくある落とし穴
アカウントプールが 1 つしかない場合、「ローテーションが救ってくれる」と期待しないでください ローテーションは「別のアカウントに切り替える」だけで、プールに 2 つ目のアカウントがない場合、429/invalid_grant はより頻繁に暴露されます。
CacheFirstは「利用可能になるまで待つ」わけではありません 現在のバックエンドスケジューリングロジックは、レート制限に遭遇するとバインディングを解除してアカウントを切り替える傾向があり、長期間ブロックして待つことはありません。固定アカウントは絶対的に強制ではありません 固定アカウントがレート制限とマークされている、またはクォータ保護にヒットしている場合、システムはラウンドロビンにフォールバックします。
このレッスンのまとめ
- スケジューリングチェーン:handler が
session_idを抽出 →TokenManager::get_tokenがアカウントを選択 → エラー時にattempt > 0で強制的ローテーション - 最もよく使う 2 つのスイッチ:スケジューリングモード(スティッキー/60秒再利用を有効にするかどうか)+ 固定アカウントモード(アカウントを直接指定)
- 429/5xx は「レート制限状態」として記録され、後続のスケジューリングでそのアカウントをスキップし、ロック時間が期限切れになるまで
次のレッスンの予告
次のレッスンでは モデルルーティング を見ます:「安定したモデルセット」を公開したい、またはワイルドカード/プリセット戦略を行いたい場合、どう設定とトラブルシューティングを行うか。
付録:ソースコード参考
クリックしてソースコードの場所を展開
更新日時:2026-01-23
| 機能 | ファイルパス | 行番号 |
|---|---|---|
| スケジューリングモードと設定構造(StickySessionConfig) | src-tauri/src/proxy/sticky_config.rs | 1-36 |
| セッションフィンガープリント生成(Claude/OpenAI/Gemini) | src-tauri/src/proxy/session_manager.rs | 1-159 |
| TokenManager:固定アカウントモードフィールドと初期化 | src-tauri/src/proxy/token_manager.rs | 27-50 |
| TokenManager:アカウント選択コアロジック(固定アカウント/スティッキーセッション/60秒ウィンドウ/ラウンドロビン/クォータ保護) | src-tauri/src/proxy/token_manager.rs | 470-940 |
| TokenManager:invalid_grant 自動無効化とプールからの削除 | src-tauri/src/proxy/token_manager.rs | 868-878 |
| TokenManager:レート制限記録と成功時クリア API | src-tauri/src/proxy/token_manager.rs | 1087-1147 |
| TokenManager:スケジューリング設定の更新 / セッションバインディングのクリア / 固定アカウントモード setter | src-tauri/src/proxy/token_manager.rs | 1419-1461 |
| ProxyConfig:scheduling フィールド定義とデフォルト値 | src-tauri/src/proxy/config.rs | 174-257 |
| リバースプロキシ起動時の scheduling 設定同期 | src-tauri/src/commands/proxy.rs | 70-100 |
| スケジューリング関連 Tauri コマンド(get/update/clear bindings/fixed account) | src-tauri/src/commands/proxy.rs | 478-551 |
| OpenAI handler:session_id + 再試行時強制的ローテーション | src-tauri/src/proxy/handlers/openai.rs | 160-182 |
| OpenAI handler:429/5xx レート制限記録 + retry delay 解析 | src-tauri/src/proxy/handlers/openai.rs | 349-367 |
| Gemini handler:session_id + 再試行時強制的ローテーション | src-tauri/src/proxy/handlers/gemini.rs | 62-88 |
| Gemini handler:429/5xx レート制限記録とローテーション | src-tauri/src/proxy/handlers/gemini.rs | 279-299 |
| Claude handler:session_id 抽出と TokenManager への転送 | src-tauri/src/proxy/handlers/claude.rs | 517-524 |
| 429 retry delay 解析(RetryInfo.retryDelay / quotaResetDelay) | src-tauri/src/proxy/upstream/retry.rs | 37-66 |
| レート制限原因識別と指数バックオフ(RateLimitTracker) | src-tauri/src/proxy/rate_limit.rs | 154-279 |
主要な構造体:
StickySessionConfig:スケジューリングモードと設定構造(mode、max_wait_seconds)TokenManager:アカウントプール、セッションバインディング、固定アカウントモード、レート制限トラッカーSessionManager:リクエストからsession_idを抽出