従来のロードバランシングがリアルタイムAIで機能しない理由
一般的なWeb APIは、リクエストとレスポンスが完了すればサーバーリソースが解放されます。そのためQPSやCPU使用率といった指標で十分でした。しかしリアルタイムAIエージェントは異なります。WebSocketやgRPCの双方向ストリーム上で、音声チャンク、文字起こし、モデル出力、TTS音声が同時に流れる長時間実行セッションなのです。
ユーザーが発話を中断した場合、サーバーは即座に音声生成を停止し、コンテキストを更新し、必要なら新しいツールを呼び出し、別の応答のドラフトを開始しなければなりません。しかも接続を切らずにです。これはもはや「リクエスト最適化」の問題ではなく、生きた会話を管理するインフラの問題です。
根拠資料: Scaling real-time AI agents with session-aware load balancing
QPSとCPUが騙す2つの罠
- Task A: 50msのリクエストを100件処理
- Task B: 20分のセッションを5件処理
QPSだけを見るとTask Aの方が忙しく見えます。しかし実際にコミットされたワークロードはTask Bの方が圧倒的です。QPSは「到着量」しか数えず、「すでに進行中の会話数」を数えられません。
CPUも同様です。音声ランタイムが無音のセッションを20件抱えている場合、CPUは遊んでいるように見えます。しかしその20人が同時に発話を始めた瞬間、CPU使用率は急上昇します。CPUは現在の圧力、アクティブセッション数は約束された将来の負荷です。両方を見る必要があります。
![]()
アクティブセッションカウンタを正しく実装する
標準的なロードバランサは、接続が「実際に会話中」なのか「アイドル状態のリスナー」なのか「ヘルスチェックのリトライ」なのかを区別できません。そのためアプリケーションレベルでセッション状態を報告する必要があります。
最もシンプルかつ堅牢なパターンは、ストリーミングセッションのライフサイクルの開始点と終了点でカウンタを増減させる方法です。
suspend fun handleAudioSession(audioStream: Flow<AudioFrame>) {
activeSessions.incrementAndGet()
try {
withTimeout(20.minutes) {
audioStream.collect { frame ->
processAndRespond(frame)
}
}
} finally {
// finallyブロックが核心です。
// これがあるからこそ、ルーティング判断に使える精度でカウントが維持されます。
activeSessions.decrementAndGet()
}
}
finallyブロックは単なるクリーンアップではありません。これが抜けると、セッション終了後もサーバーが過負荷に見えてしまいます。逆に二重デクリメントが発生すると、存在しない容量を報告して過剰なトラフィックを引き込んでしまいます。タイムアウト、キャンセル、切断が同時に発生するエッジケースも必ず処理してください。
ハイブリッド容量モデル:セッションとCPUを統合する
静的なスロットモデルは次のようになります。
remaining_capacity = max_sessions - active_sessions
100スロット中80が埋まっていれば残り20?しかし、すべてのセッションが同じCPUを消費するという仮定は、生成AIではほぼ成り立ちません。そこで正規化されたハイブリッドモデルが必要になります。
ロードバランサはrate単位で思考するため、静的なセッション数をrateに変換します。例えば10秒のレポーティングウィンドウでアクティブセッションが90件なら、**「擬似QPS 9」**として扱うわけです。
Cost_Per_Session = (Current_Utilization - Target_Utilization) / Active_Sessions
Additional_Session_Rate = (Target_Utilization - Current_Utilization) / Cost_Per_Session * Safety_Scaler
Effective_Capacity = Active_Sessions + (Additional_Session_Rate * Reporting_Interval)
この式が実務でどう機能するかというと:
- セッション10件 + CPU 90% → Cost_Per_Sessionが非常に高くなり、Additional_Session_Rateは0に。新規トラフィックを一切受け付けません。
- セッション80件 + CPU 40% → 余裕があるように見えますが、Safety_Scalerが段階的にしか新規セッションを流しません。急激なスパイクを防ぐためです。
重要なのは現在の状態の重みと、コミット済みセッションの量を同時に理解することです。

ベンチマークとカウンタ設計で、多くのエンジニアが躓くポイント
fire-and-forget型の負荷テストは罠です
短いリクエストを大量に投げる負荷テストは、スループットを測るだけで、長時間セッションの障害モードを再現できません。ベンチマークは必ず多様化させる必要があります。
- 短いリクエストと長時間セッションの混在
- セッション寿命の分布(p50, p95, p99)
- 強制切断シナリオ
- 同時割り込みのバースト
計測すべきストリーミング指標
平均レイテンシとQPSを超えて、以下を追跡してください。
- バックエンドごとのアクティブセッション分布
- 過負荷割り当て率(overloaded assignment rate)
- p95/p99 起動レイテンシ
- time-to-first-stream(最初のストリームまでの時間)
- ドロップされたセッション数
- 強制切断後のカウンタ挙動
カウンタ自体がボトルネックになり得ます
すべてのストリームの開始と終了がカウンタを叩きます。大規模同時実行では、このトラッカーがサービスのボトルネックにならないよう設計する必要があります。
JVMの場合、AtomicIntegerは多くのワークロードで十分ですが、高並行環境ではキャッシュライン競合(cache-line bouncing)によりスループットが急落する可能性があります。複数のスレッドが同じメモリアドレスを更新し続けるためです。そのような場合はsharded counterやLongAdderスタイルの集約が適切です。
マイクロベンチマークを実施する際は、JMHなどのフレームワークでJIT最適化、JVMウォームアップ、dead-code eliminationを必ず統制してください。単一スレッドのレイテンシよりも競合シナリオに集中する方が、実務的に遥かに有用です。
この技術の限界と注意事項
- ネットワークパーティション: ロードバランサがバックエンドのメトリクスを受信できない場合、セッションカウントがstaleになり、誤ったルーティングが発生します。フォールバック戦略(例:最後のスナップショット + TTL)を必ず設計してください。
- プロキシごとの差異: Envoy、NGINX、HAProxyではメトリクスのポーリング間隔と集約方式が異なります。特定のプロキシに依存したチューニングは移植性が低くなります。
- オーバーエンジニアリングの境界: セッション数が数百程度なら、ハイブリッドモデルまで導入する必要はありません。QPS + CPUで十分です。この戦略は数千の同時セッションから真価を発揮します。
次のステップ学習の方向性
- Envoyの
least_requestとカスタムセッションベースポリシーの比較実験 - OpenTelemetryでセッションライフサイクルトレーシングを導入
- LongAdder vs sharded counterの実負荷ベンチマーク
- Kubernetes HPAにカスタムメトリクス(active_sessions)を連携してオートスケーリングトリガーを構築
リアルタイムAIインフラは「リクエストバランシング」から「生きた会話のバランシング」へと移行しています。QPSとCPUは、もはや出発点に過ぎません。

まとめ:3つのシグナルを同時に見る
- QPS: 到着量
- CPU: 現在の圧力
- アクティブセッション数: コミットされた真の同時実行性
これら3つを1つのモデルに統合した瞬間、ロードバランサは格段に賢くなります。特定のインスタンスがボトルネックになるのを防ぎ、アイドル状態のサーバーへ自然にトラフィックを分散できます。
リアルタイムAIエージェントがプロダクションに投入されつつある今、インフラも「離散的なリクエスト」から「連続的な会話」へと思考を転換する必要があります。