メインコンテンツにスキップ
OpenAI

OpenAI が大規模環境で低遅延の音声 AI を実現する方法

技術スタッフの Yi Zhang と William McDonald による寄稿

音声 AI が自然に感じられるのは、会話が発話のテンポで進む場合に限られます。ネットワークに問題があると、不自然な間や音声の途切れ、割り込み発話(barge-in)の遅延として、ユーザーはすぐに違和感を覚えます。これは、ChatGPT の音声モードだけでなく、Realtime API を利用する開発者、対話型ワークフローで動作するエージェント、さらにユーザーの発話中に音声を処理する必要があるモデルにとっても重要です。

OpenAI の規模でこれを実現するには、次の3つの要件を満たす必要があります。

  • 9億人を超える週間アクティブユーザーに対応できるグローバルな接続性
  • セッション開始直後からユーザーが話し始められる高速な接続確立
  • ジッターやパケットロスを抑えた、低く安定したメディア往復遅延によるスムーズな会話の切り替え

OpenAI でリアルタイム AI 対話を担当するチームは最近、スケール拡大に伴って顕在化した3つの制約に対応するため、WebRTC スタックを再設計しました。具体的には、セッションごとに1ポートを使うメディア終端方式が OpenAI のインフラに適していなかったこと、ステートフルな ICE(Interactive Connectivity Establishment)および DTLS(Datagram Transport Layer Security)セッションには安定した所有先が必要だったこと、さらにグローバルルーティングではファーストホップのレイテンシを低く維持する必要があったことです。本記事では、クライアント側では標準的な WebRTC の動作を維持しながら、OpenAI のインフラ内部におけるパケットルーティングを刷新するために構築した、リレーとトランシーバーを分離したアーキテクチャについて解説します。

WebRTC がリアルタイム AI 製品を支える

WebRTC は、ブラウザ、モバイルアプリ、サーバー間で低遅延の音声・映像・データを送受信するためのオープン標準です。WebRTC はピアツーピア通話の技術として知られることが多いものの、クライアント・サーバー型のリアルタイムシステムにとっても実用的な基盤です。これは、接続確立と NAT(Network Address Translation)越えを行う ICE、暗号化通信のための DTLS と SRTP(Secure Real-time Transport Protocol)、音声圧縮・復号のためのコーデックネゴシエーション、品質制御を担う RTCP(Real-time Transport Control Protocol)、さらにエコーキャンセルやジッターバッファといったクライアント側機能など、インタラクティブメディアで難易度の高い部分を標準化しているためです。

こうした標準化は、AI 製品において特に重要です。WebRTC がなければ、各クライアントごとに、NAT を越えた接続確立、メディアの暗号化、コーデック(送信と復号に用いる符号化・復号方式)のネゴシエーション、さらには変化するネットワーク状況への適応方法を個別に実装する必要があります。WebRTC を利用することで、ブラウザやモバイルプラットフォームですでに実装されているプロトコルスタックを活用できるため、私たちはリアルタイムメディアをモデルへ接続するインフラの構築に集中できます。

また私たちは、成熟したオープンソース実装や、ブラウザ・モバイルアプリ・サーバー間の相互運用性を維持する標準化活動を含む、WebRTC エコシステムそのものを基盤として活用しています。WebRTC の初期アーキテクトの一人である Justin Uberti と、Pion の開発者兼メンテナーである Sean DuBois による基盤的な取り組みによって、私たちのようなチームは、低レベルのトランスポート、暗号化、輻輳制御の挙動をゼロから実装し直すのではなく、実運用で十分に検証されたメディアインフラを基盤として活用できています。幸運なことに、Justin と Sean は現在 OpenAI の同僚として、WebRTC とリアルタイム AI の統合をさらに進める取り組みを支えています。

AI にとって最も重要なのは、音声が途切れないストリームとして到着することです。音声エージェントは、音声全体のアップロード完了を待つことなく、ユーザーが話している最中から文字起こし、推論、ツール呼び出し、音声生成を開始できます。これこそが、自然に会話できるシステムと、プッシュ・トゥ・トークのように感じられるシステムとの差です。

メディアアーキテクチャの選定

WebRTC の採用を決めた後、次に課題となったのは、それをどこで終端するか(たとえばエッジで WebRTC 接続を受け入れ、管理する場所)と、それらのセッションをどのように推論バックエンドへ接続するかでした。終端方式は、リアルタイムのセッション状態、メディア転送、ルーティング、レイテンシ、障害分離をどのように扱うかを左右するため、重要です。

オプション1:SFU アプローチでは、AI が WebRTC 参加者として動作します

SFU(Selective Forwarding Unit)は、各参加者から WebRTC ストリームを受信し、それを他の参加者へ選択的に転送するメディアサーバーです。このモデルでは、SFU が参加者ごとに個別の WebRTC 接続を終端し、AI はセッション内の別の参加者として参加します。この方式は、グループ通話、オンライン授業、共同作業ミーティングなど、本質的に複数参加者を前提とする製品に適しています。この構成では、オーディオコーデック、RTCP メッセージ、データチャネル、録音機能、ストリームごとのポリシーを一元管理できます。1

クライアントと AI を接続する製品であっても、SFU はしばしば標準的な出発点になります。これは、シグナリング、メディアルーティング、録画、オブザーバビリティに加え、人間への引き継ぎや参加者追加といった将来的な拡張まで、実績ある単一システムを再利用できるためです。

オプション2:トランシーバーアプローチでは、エッジで WebRTC を終端し、バックエンドプロトコルへ変換します

一方で、OpenAI のワークロードは異なります。OpenAI の大半のセッションは 1対1 です。つまり、1人のユーザーが1つのモデルと対話する、あるいは1つのアプリケーションが1つのリアルタイムエージェントと対話する形式であり、会話の各ターンで低レイテンシが求められます。このようなトラフィック特性に対して、私たちはトランシーバーモデルを採用しました。WebRTC エッジサービスがクライアント接続を終端し、その後、メディアやイベントを、モデル推論、文字起こし、音声生成、ツール利用、オーケストレーション向けのよりシンプルな内部プロトコルへ変換します。

この設計では、ICE 接続チェック、DTLS ハンドシェイク、SRTP の暗号鍵、セッションライフサイクルを含む WebRTC セッション状態を管理するのは、トランシーバーのみです。ここでいう「終端」とは、トランシーバーがそれらのハンドシェイクを完了し、メディアの暗号化・復号を行うエンドポイントになることを意味します。セッション状態を一か所に集約することで、セッションの管理責任を明確にしやすくなりました。また、バックエンドサービスを WebRTC ピアとして直接動作させる必要がなくなり、通常のサービスと同様にスケールできるようになりました。

デプロイにおける核心的な課題:WebRTC と Kubernetes の統合

トランシーバーモデルを採用した後、最初に実装したのは、Pion を基盤とした単一の Go サービスで、シグナリングとメディア終端の両方を処理するものでした。このサービスは、ChatGPT の音声機能、Realtime API の WebRTC エンドポイント、さらに複数の研究プロジェクトを支えています。

運用面では、トランシーバーサービスは次の2つの役割を担います。

  • シグナリング:SDP ネゴシエーション、コーデック選択、ICE 認証情報、セッションセットアップ
  • メディア:ダウンストリーム側の WebRTC 接続を終端し、推論やオーケストレーションのためにバックエンドサービスとのアップストリーム接続を維持

私たちは、このサービスを他のインフラと同様に Kubernetes 上で運用したいと考えていました。Kubernetes では、需要に応じてワークロードをスケールアップ・スケールダウンしたり、ホスト間で移動したりできます。しかし、従来の「セッションごとに1ポートを割り当てる」WebRTC モデルは、この環境には適していません。これは、Pod の追加・削除・再スケジュール時にも維持しなければならない、大規模な公開 UDP ポート範囲に依存しており、それらの公開、保護、維持が難しいためです。22

ポート枯渇

最初の課題は、「1セッションにつき1ポート」を割り当てるモデルそのものでした。同時接続数が増えると、非常に広範な UDP ポート範囲を公開・管理する必要があります。

  • クラウドロードバランサーや Kubernetes Service は、1サービスあたり数万規模の公開 UDP ポートを扱う前提では設計されていません。ポート範囲が増えるたびに、ロードバランサー設定、ヘルスチェック、ファイアウォールポリシー、ロールアウト時の安全性といった運用面の複雑さも増していきます。3
  • UDP ポート範囲が広がると、外部から到達可能な領域が拡大し、ネットワークポリシーの監査も難しくなるため、セキュリティ確保が困難になります。
  • また、これはオートスケーリングとの相性もよくありません。Kubernetes では、Pod の追加、削除、再スケジュールが常に発生します。各 Pod に対して広範かつ固定的なポート範囲の予約と公開を求めると、この柔軟性が損なわれます。4

そのため、多くの WebRTC システムでは、サーバーごとに単一の UDP ポートを使用し、その背後でアプリケーションレベルのデマルチプレクシングを行う構成へ移行しています。5

状態の固定性

サーバーごとに単一ポートを使う設計は、ポート数の問題を解決できますが、別の課題も生じます。それが、フリート全体で各セッションの所有先を維持することです。

ICE と DTLS はステートフルなプロトコルです。セッションを生成したプロセスは、その後も同じセッションのパケットを継続して受信し、接続性チェックの検証、DTLS ハンドシェイクの完了、SRTP の復号、さらに ICE 再起動など後続のセッション変更を処理する必要があります。同じセッションのパケットが別のプロセスへ振り分けられると、セットアップに失敗したり、メディア通信が途切れたりする可能性があります。

そこで私たちは、公開インターネットに対しては小規模で固定的な UDP 範囲だけを提供しつつ、すべてのパケットを対応する WebRTC セッションを管理するトランシーバーへ確実にルーティングすることを目標に据えました。

WebRTC メディアアーキテクチャの比較

私たちは、この実現方法として複数のアプローチを検討しました。その1つが TURN(Traversal Using Relays around NAT)です。これは、エッジリレーがクライアントのアロケーションを終端し、クライアントに代わってトラフィックを転送する方式です。2

Approach

メリット

デメリット

セッションごとに一意の IP:port(ネイティブ direct UDP とも呼ばれます)

クライアントからサーバーへの直接メディアパス

データパス上に転送レイヤーが存在しません

セッションごとに1つの公開 UDP ポートが必要です

広範なポート範囲は、公開やセキュリティ確保が困難です

Kubernetes やクラウドロードバランサーとの相性がよくありません

サーバーごとに一意の IP:port

セッション単位で公開する場合と比べて、公開 UDP 範囲を大幅に小さくできます

サーバーごとの1つの共有ソケットで、多数のセッションを振り分けできます。

単一ホストでは問題なく動作しますが、それ単体では、ロードバランスされた共有フリート全体には対応できません

単一ホスト上でのセッションの振り分けは、そのホストへパケットが到達した後にしか機能しません。ロードバランスされたフリート全体では、最初のパケットが誤ったインスタンスへ届く可能性があるため、各セッションを対応するプロセスへ決定論的に振り分ける仕組みが引き続き必要です。


TURN リレー(プロトコル終端型)

クライアントは TURN リレーのアドレスとポートへ到達できれば十分です

エッジでポリシーを一元管理できます

TURN アロケーションによってセットアップ時のラウンドトリップが増加します

TURN サーバー間でアロケーションを移動・復旧することは依然として困難です

ステートレスフォワーダー + ステートフルターミネーター(OpenAI のリレー + トランシーバー)

小規模な公開 UDP 範囲

トランシーバーが引き続き WebRTC セッション全体を管理します

メディアが対象トランシーバーへ到達する前に、転送ホップが1つ追加されます

リレーとトランシーバー間で独自の連携処理が必要です

アーキテクチャ概要:リレー + トランシーバー

私たちが導入したアーキテクチャでは、パケットルーティングとプロトコル終端を分離しています。シグナリングは引き続きセッションセットアップのためにトランシーバーへ到達しますが、メディアはまずリレーを経由して流れます。リレーは、公開範囲を最小限に抑えた軽量な UDP 転送レイヤーであり、その背後でトランシーバーがステートフルな WebRTC エンドポイントとして動作します。

リレーはステートレスにパケットをトランシーバーへ転送します

リレーは、メディアの復号、ICE ステートマシンの実行、コーデックネゴシエーションには関与しません。リレーは、宛先決定に必要な最小限のパケットメタデータだけを読み取り、その後、対応するセッションを管理するトランシーバーへパケットを転送します。トランシーバー側から見ると、通常の WebRTC フローとして扱われ、すべてのプロトコル状態も引き続き保持されます。クライアント側から見た WebRTC セッションの挙動は変わりません。

ICE 認証情報を利用したルーティング

この構成で重要になるのが、最初のパケットのルーティングです。リレーは、外部ルックアップサービスへ問い合わせて待機するのではなく、パケット経路上で直接、まだセッションが存在しない段階からクライアントの最初のパケットをルーティングする必要があります。

すべての WebRTC セッションには、プロトコル標準のルーティング用フックがすでに含まれています。それが ICE username fragment、つまり ufrag です。これはセッションセットアップ時に交換され、STUN 接続性チェックでも使われる短い識別子です。私たちは、リレーが宛先クラスターと対応するトランシーバーを推定できるよう、必要最小限のルーティングメタデータだけを含むサーバー側 ufrag を生成しています。

シーケンス図は、接続がどのように確立されるかを示しています。

シグナリング時には、トランシーバーがセッション状態を確保し、共有リレー VIP と UDP ポートを SDP アンサー内で返します。VIP は、リレーフリートの前面に配置される仮想 IP アドレスです。ポートと組み合わせることで、その背後に複数のリレーインスタンスが存在していても、203.0.113.10:3478 のような単一で安定した宛先をクライアントへ提供できます。クライアントが最初に送信するメディアパス上のパケットは、通常、STUN(Session Traversal Utilities for NAT)のバインディングリクエストです。ICE はこれを使って、通知されたアドレスへパケットが到達可能かを確認します。

リレーは、最初の STUN パケットを必要最小限だけ解析し、サーバー側 ufrag を読み取り、ルーティングヒントをデコードして、対応するトランシーバーへパケットを転送します。各トランシーバーは共有 UDP ソケットで待ち受けます。つまり、セッションごとにソケットを持つのではなく、内部 IP:port にバインドされた単一の OS エンドポイントを使用します。リレーがクライアントの送信元 IP:port から対象トランシーバーへのセッションを確立すると、その後の DTLS、RTP、RTCP パケットは、ufrag を再デコードすることなく、そのセッション内で処理されます。

リレー側のセッションは、意図的に最小限の構成にしています。パケット転送に必要なインメモリセッションに加え、監視用カウンターと、セッション期限切れやクリーンアップ用タイマーのみを保持します。この設計により、パケットルーティングをパケット経路上で直接処理できます。リレーが再起動してセッション情報を失った場合でも、次の STUN パケットを使って ufrag のルーティングヒントからセッションを再構築できます。さらに信頼性を高めるため、ルート確立後は <クライアント IP + ポート、トランシーバー IP + ポート> のマッピングを Redis キャッシュに保持しています。これにより、次の STUN パケットを待たずに、より早くセッションを復元できます。

Global Relay と地理的に誘導されるシグナリング

外部に公開する UDP の範囲を、少数の固定アドレスとポートに絞り込むことで、同じリレーパターンをグローバルに展開できるようになりました。Global Relay は、地理的に分散配置されたリレーイングレス群であり、すべて同じパケット転送動作を実装しています。

地理的に広範なイングレスを配置することで、クライアントから OpenAI までの最初のホップを短縮できます。これは、パケットが最初にパブリックインターネット経由で遠隔リージョンへ到達するのではなく、地理的にもネットワークトポロジー上でもユーザーに近いリレーから OpenAI のネットワークへ入れるためです。実際には、これによりトラフィックが OpenAI のバックボーンへ到達する前の段階で、レイテンシ低下、ジッター抑制、回避可能なパケットロスバーストの削減につながります。6

Global Relay レイヤーは、クライアントからパケットを受信し、トランシーバークラスターへ転送します

シグナリングには Cloudflare の Geo Steering と Proximity Steering を利用しており、最初の HTTP または WebSocket リクエストが近くのトランシーバークラスターへ到達するようにしています。リクエストコンテキストによって、セッションを配置する場所と、クライアントへ通知する Global Relay のイングレスポイントが決まります。SDP アンサーには Global Relay のアドレスが含まれ、ufrag には、Global Relay がメディアを指定されたクラスターへルーティングし、リレーが対象トランシーバーへ転送するために必要な情報が含まれます。

地理的に誘導されるシグナリングと Global Relay を組み合わせることで、セッションを1つのトランシーバーに固定したまま、セットアップとメディアの両方を近い経路で処理できます。これにより、シグナリングと最初の ICE 接続性チェックのラウンドトリップ時間が短縮され、ユーザーが発話を開始するまでの時間も短くなります。

リレーの実装とパフォーマンス

リレーサービスは Go で実装し、意図的に実装範囲を絞り込みました。Linux では、カーネルのネットワークスタックがマシンのネットワークインターフェイスから UDP パケットを受信し、それをソケットへ渡します。ソケットとは、プロセスが IP:port をバインドした後に読み書きする OS レベルのエンドポイントです。リレーはユーザー空間で動作するため、通常の Go アプリケーションがそのソケットからパケットヘッダーを読み取り、最小限のフロー状態を更新しながら、WebRTC を終端せずにパケットを転送します。カーネルバイパスフレームワークを採用する必要はありませんでした。これは、ユーザー空間プロセスがネットワークキューを直接ポーリングして高いパケットレートを実現できる一方で、運用の複雑さも増してしまうためです。

主な設計上の選択

  • プロトコル終端を行わない:リレーは STUN ヘッダーと ufrag のみを解析し、その後の DTLS、RTP、RTCP にはキャッシュ済み状態を利用することで、パケット内容を解釈せずに処理します。
  • 一時的な状態管理:フロー状態とオブザーバビリティのために、クライアントアドレスとトランシーバー宛先を対応付ける、小規模かつ短時間でタイムアウトするインメモリマップを保持します。
  • 水平スケーラビリティ:複数のリレーインスタンスをロードバランサー配下で並列稼働させます。保持している状態は WebRTC のハードステートではないため、再起動時のトラフィック断は最小限に抑えられ、フローも迅速に回復します。

効率化施策

  • SO_REUSEPORT は、同一マシン上の複数のリレーワーカーが同じ UDP ポートへバインドできる Linux のソケットオプションです。カーネルは受信パケットを各ワーカーへ分散するため、単一の読み取りループがボトルネックになることを防げます。
  • runtime.LockOSThread は、UDP を読み取る各 goroutine を特定の OS スレッドへ固定します。SO_REUSEPORT と組み合わせることで、同一フロー(送信元および宛先の IP:port とプロトコル)のパケットを同じ CPU コア上で処理しやすくなり、キャッシュ局所性の向上とコンテキストスイッチ削減につながります。
  • 事前割り当てしたバッファと最小限のコピーによって、パースやメモリ割り当てのオーバーヘッドを抑え、Go におけるガベージコレクションの発生を最小限にしています。

この実装により、比較的小規模なリレー基盤でグローバルなリアルタイムメディアトラフィックを処理できたため、カーネルバイパス方式へ移行するのではなく、よりシンプルな設計を維持しました。

結果と学び

このアーキテクチャにより、数千もの UDP ポートを公開することなく、Kubernetes 上で WebRTC メディアを運用できるようになりました。これは、小規模かつ固定的な UDP 範囲のほうがセキュリティ確保や負荷分散を行いやすく、大規模な公開ポート範囲を確保しなくてもインフラをスケールできるためです。Kubernetes によるインフラサポートの向上と、攻撃対象領域の縮小によるセキュリティ強化に加え、この設計ではクライアント側の標準的な WebRTC 動作も維持できました。また、SFU を使わない設計が、私たちのワークロードに適したデフォルト構成だったことも確認できました。私たちのセッションの大半はポイントツーポイント型であり、レイテンシに敏感です。また、推論サービスが WebRTC ピアとして振る舞う必要がない構成のほうが、スケールしやすいことも分かりました。

より広い観点で得られた教訓は、複雑さを加えるべき場所は、各バックエンドサービスやカスタムクライアント動作ではなく、薄いルーティングレイヤーであるということです。ルーティングメタデータをプロトコルネイティブなフィールドへ埋め込むことで、決定論的な初回パケットルーティング、小規模な公開 UDP 範囲、そして世界中のユーザー近くへイングレスを配置できる十分な柔軟性を実現できました。

特に重要だった選択肢がいくつかあります。

  • エッジでプロトコルセマンティクスを維持すること。クライアントは引き続き標準的な WebRTC を利用するため、ブラウザとモバイル間の相互運用性を維持できます。
  • ハードなセッション状態を1か所へ集約すること。トランシーバーが ICE、DTLS、SRTP、およびセッションライフサイクルを管理し、リレーはパケット転送のみを担当します。
  • セットアップ時点ですでに存在する情報を利用してルーティングすること。ICE ufrag によって、ホットパス上にルックアップ依存を追加することなく、初回パケット用のルーティングフックを実現できました。
  • カーネルバイパスを導入する前に、まず一般的なケースに対して最適化すること。SO_REUSEPORT の慎重な利用、スレッドピンニング、低アロケーションなパースを組み合わせた、シンプルな Go 実装で、私たちのワークロードには十分対応できました。

リアルタイム音声 AI は、インフラ側で遅延をほとんど意識させないことによって初めて成立します。私たちにとってそれは、クライアントが WebRTC 自体に期待する挙動を変えることなく、WebRTC デプロイメントの構成を変えることを意味していました。