デュアルチャネル分離でモデル可用率を 97% から 99.8% へ
単一供給チャネルの可用率の天井はどこか。18 か月分の本番データを振り返り、デュアルチャネル分離の実装と効果を分解します。
問題:単一チャネルの天井
2024 年末、私たちの中核モデル(Claude Sonnet、GPT-4o)は単一チャネルで上流プロバイダーに接続していました。月次可用率は 96.5%〜97.2% の間で揺れていました。十分そう?
計算すれば違うと分かります。97% の可用率は月に約 21 時間 の停止を意味します。私たちをインフラとして使うチームにとって、この数字は受け入れられません。
問題の根は私たちのコードではなく、上流プロバイダー自体の変動です。1 つのチャネルが落ちれば、すべてのトラフィックが影響を受けます。単一依存点はアーキテクチャの問題であって、運用の問題ではありません。
設計:デュアルチャネル分離
中核の発想はシンプルです。モデルごとに独立した供給チャネルを 2 本(Channel A / Channel B)維持し、互いに fallback とします。
ルーティングロジック
リクエストが来ると、ルーターは 3 つのことをします:
- 両チャネルのリアルタイム健康スコアを照会(直近 5 分のエラー率と遅延に基づく)
- 健康スコアの高いチャネルを優先
- 主チャネルが 5xx またはタイムアウトを返したら、即座に副チャネルへ再試行(ほとんどの場合ユーザーには無感知)
async function route(model: string, req: Request) {
const [chA, chB] = await getChannels(model)
const primary = chA.healthScore >= chB.healthScore ? chA : chB
const fallback = primary === chA ? chB : chA
try {
return await primary.forward(req, { timeout: 30_000 })
} catch (e) {
// 主チャネル失敗、即切替——ユーザーには無感知
metrics.record('failover', { model, from: primary.id })
return await fallback.forward(req, { timeout: 30_000 })
}
}
健康スコアの計算
各チャネルの健康スコアは 0〜100 のスライディングウィンドウ値です:
- エラー率 重み 70% — 直近 5 分の 5xx / 総リクエスト数
- 遅延 重み 30% — P95 遅延のベースラインからの乖離
スコアが 50 未満のチャネルは自動的に重みを下げ、3 分連続で 30 未満のチャネルは一時的にルーティングプールから外し、独立したハートビート探針で周期的に試し、回復後に再投入します。
結果:18 か月分の本番データ
デュアルチャネル分離の公開後、可用率は 99.81% に向上し、障害復旧時間は p95 4 分から p95 8 秒に短縮されました。大多数のユーザーはプロバイダー側の障害にまったく気づきませんでした。
| 指標 | 単一チャネル(2024 Q4) | デュアルチャネル(2025 Q3) |
|---|---|---|
| 可用率 | 97.0% | 99.81% |
| 月平均停止時間 | 21h | 1.4h |
| failover p95 | — | 8s |
踏んだ落とし穴
- 初期はタイムアウト予算を分離していなかった:副チャネルの timeout も 30 秒で、障害時に待ち時間が倍に。主 30 秒 / 副 20 秒に変えてから体感遅延が 35% 低下。
- 健康スコアの遅延問題:5 分ウィンドウは突発障害への反応が遅く、後に 30 秒の「直近の失敗」高速シグナルを重ねました。
- fallback ストーム:主チャネルの瞬間的な揺れで両側がポーリングし副チャネルを圧迫。クライアント側のレート制限 + バックオフで緩和。
次のステップ
デュアルチャネルは 単一ベンダー障害 をすでに解決しました。しかし私たちは依然として単一のネットワーク出口、単一の課金アカウント、単一のリージョンに依存しています。次のロードマップはこれらの層に分離を広げること——実装し終えたら 1 本書きます。