Переход к основному контенту
OpenAI

4 мая 2026 г.

Инженерия

Как OpenAI обеспечивает работу голосового ИИ с низкой задержкой при больших нагрузках

И Чжан и Уильям Макдональд, технические специалисты

Голосовой ИИ кажется естественным только тогда, когда беседа идет со скоростью живой речи. Как только возникают сетевые помехи, человек моментально это замечает: появляются неловкие паузы, обрезанные фразы или запоздалые реакции на попытку вклиниться в разговор. Это критически важно для ChatGPT Разговор, для разработчиков, создающих продукты на базе Realtime API, для агентов в интерактивных рабочих процессах и для моделей, которым нужно обрабатывать аудио прямо во время разговора пользователя.

В масштабе OpenAI это означает три конкретных требования:

  • Глобальный охват для более чем 900 миллионов еженедельно активных пользователей
  • Быстрое установление соединения, чтобы пользователь мог начать говорить сразу после начала сеанса
  • Низкое и стабильное время прохождения медиатрафика в оба конца, с низким джиттером и потерей пакетов, чтобы смена реплик ощущалась четкой

Команда OpenAI, отвечающая за взаимодействия с ИИ в реальном времени, недавно переработала наш стек WebRTC, чтобы решить три ограничения, которые стали конфликтовать при масштабировании: приземление трафика по принципу «один порт на сессию» плохо подходит инфраструктуре OpenAI, сеансам ICE (Interactive Connectivity Establishment) и DTLS (Datagram Transport Layer Security) с сохранением состояния требуется стабильная привязка к узлу, а глобальная маршрутизация должна удерживать низкую задержку первом сегменте пути. В этой статье мы расскажем об архитектуре с разделением на реле (relay) и трансивер (transceiver), которую мы создали, чтобы сохранить стандартное поведение WebRTC для клиентов, изменив при этом логику маршрутизации пакетов внутри инфраструктуры OpenAI.

WebRTC позволяет нам создавать ИИ-продукты, работающие в реальном времени

WebRTC — это открытый стандарт для передачи аудио, видео и данных с низкой задержкой между браузерами, мобильными приложениями и серверами. Его часто связывают с peer-to-peer-звонками (звонками напрямую), но он также служит практической основой для клиент-серверных систем реального времени, потому что стандартизирует самые сложные части интерактивных медиа: ICE для установления связности и обхода NAT (Network Address Translation), DTLS и SRTP (Secure Real-time Transport Protocol) для шифрованной передачи, согласование кодеков для сжатия и декодирования аудио, RTCP (Real-time Transport Control Protocol) для контроля качества, а также клиентские функции, такие как эхоподавление и буферизация джиттера.

Эта стандартизация важна для ИИ-продуктов. Без WebRTC каждому клиенту нужен был бы свой ответ на вопрос, как устанавливать соединение через NAT, шифровать медиапоток, согласовывать кодеки (кодеры-декодеры, выбранные для передачи и распаковки) и адаптироваться к меняющимся условиям сети. С WebRTC мы можем опираться на стек протоколов, уже реализованный в браузерах и на мобильных платформах, сосредоточив собственную работу на инфраструктуре, которая связывает медиапотоки реального времени с моделями.

Мы также опираемся на саму экосистему WebRTC, включая зрелые реализации с открытым исходным кодом и работу по стандартизации, которая обеспечивает совместимость браузеров, мобильных приложений и серверов. Фундаментальная работа Джастина Уберти (одного из первоначальных архитекторов WebRTC) и Шона Дюбуа (создателя и основного разработчика Pion) позволила таким командам, как наша, строить решения на проверенной медиаинфраструктуре, а не заново изобретать низкоуровневую транспортную логику, шифрование и управление перегрузкой. Нам повезло, что и Джастин, и Шон теперь наши коллеги в OpenAI и помогают направлять сближение WebRTC и ИИ реального времени.

Для ИИ важнейшее свойство в том, что аудио поступает как непрерывный поток. Голосовой агент может начать транскрибировать, рассуждать, вызывать инструменты или генерировать речь, пока пользователь еще говорит, вместо того чтобы ждать полной загрузки. В этом разница между системой, которая ощущается как разговор, и и той, что работает по принципу рации (push-to-talk).

Выбор медиаархитектуры

После выбора WebRTC следующим вопросом стало, где завершать соединение (где мы будем принимать и удерживать WebRTC-соединение — например, на периферии) и как связывать эти сеансы с бэкенд-инфраструктурой инференса. Точка завершения важна, потому что она определяет, как мы обрабатываем состояние сеанса реального времени, транспорт медиаданных, маршрутизацию, задержку и изоляцию сбоев.

Вариант 1: подход с SFU включает ИИ как участника WebRTC

SFU (Selective Forwarding Unit) — это медиасервер, который получает по одному потоку WebRTC от каждого участника и выборочно пересылает потоки остальным. В этой модели SFU завершает отдельное WebRTC-соединение для каждого участника, а ИИ подключается как еще один участник сеанса. Это может хорошо подходить продуктам, которые по своей природе многопользовательские, например групповым звонкам, учебным занятиям или совместным встречам. Такой подход держит аудиокодеки, сообщения RTCP, каналы данных, запись и политики для отдельных потоков в одном месте.1

Даже в продуктах формата «клиент — ИИ» SFU часто является отправной точкой по умолчанию, потому что позволяет командам повторно использовать одну проверенную систему для сигналинга, маршрутизации медиатрафика, записи, наблюдаемости и будущих расширений, таких как передача разговора человеку или добавление новых участников.

Вариант 2: подход с трансивер завершает WebRTC на периферии и преобразует его во внутренний протокол

Наша нагрузка отличается. Большинство сеансов — один пользователь общается с одной моделью или одно приложение взаимодействует с одним агентом реального времени, — и здесь критична задержка при каждой смене реплик. Для такого профиля трафика мы выбрали модель трансивера: периферийный сервис WebRTC завершает клиентское соединение, а затем преобразует медиаданные и события в более простые внутренние протоколы для инференса модели, транскрипции, генерации речи, использования инструментов и оркестрации.

В этой конструкции трансивер — единственный сервис, который владеет состоянием WebRTC-сессии, включая проверки связности ICE, DTLS-хендшейки, ключи шифрования SRTP и жизненный цикл сеанса. Здесь «завершение» означает, что трансивер является конечной точкой, которая выполняет эти хендшейки и шифрует или расшифровывает медиапоток. Хранение этого состояния в одном месте упростило понимание владения сеансом и позволило бэкенд-сервисам масштабироваться как обычным сервисам, а не выступать WebRTC-пирами самостоятельно.

Основная проблема развертывания: WebRTC встречается с Kubernetes

После выбора модели трансивера наша первая реализация представляла собой единый сервис на Go, построенный на Pion, который обрабатывал и сигналинг, и завершение медиасеансов. Он обеспечивает работу ChatGPT Разговор, конечной точки WebRTC в Realtime API и ряда исследовательских проектов.

С операционной точки зрения сервис-трансивер выполняет две задачи:

  • Сигналинг: согласование SDP, выбор кодеков, учетные данные ICE и настройка сеанса
  • Медиаданные: завершение нисходящих WebRTC-соединений и поддержание восходящих соединений с бэкенд-сервисами для инференса и оркестрации

Мы хотели, чтобы сервис работал так же, как остальная наша инфраструктура: в Kubernetes, где нагрузки могут масштабироваться вверх и вниз и перемещаться между хостами по мере изменения спроса. Но традиционная модель WebRTC с одним портом на сеанс плохо вписывается в такую среду, потому что зависит от больших диапазонов публичных UDP-портов, которые сложно публиковать, защищать и сохранять при добавлении, удалении или перераспределении подов.2

Исчерпание портов

Первой проблемой была сама модель «один порт на сеанс». При высокой параллельности это означает необходимость открывать и управлять очень большими диапазонами UDP-портов.

  • Облачные балансировщики нагрузки и сервисы Kubernetes не рассчитаны на десятки тысяч публичных UDP-портов на сервис. Каждый дополнительный диапазон добавляет операционную сложность в конфигурации балансировщика, проверках состояния, политике межсетевого экрана и безопасности развертываний.3
  • Большие диапазоны UDP-портов трудно защищать, потому что они расширяют внешне доступную поверхность и усложняют аудит сетевой политики.
  • Они также плохо подходят для автоскейлинга. В Kubernetes поды постоянно добавляются, удаляются и перераспределяются. Требование, чтобы каждый pod резервировал и объявлял большой стабильный диапазон портов, делает такую эластичность хрупкой.4

Именно поэтому многие системы WebRTC переходят к одному UDP-порту на сервер с мультиплексированием на уровне приложения за этим портом.5

Привязка состояния

Конструкции с одним портом на сервер решают проблему числа портов, но создают вторую проблему: сохранение владения каждым сеансом по всему флоту.

ICE и DTLS — это протоколы с состоянием. Процесс, создавший сеанс, должен продолжать получать пакеты этого сеанса, чтобы подтверждать проверки связности, завершать DTLS-хендшейк, расшифровывать SRTP и обрабатывать последующие изменения сеанса, такие как перезапуски ICE. Если пакеты одного и того же сеанса попадут в другой процесс, установка может завершиться неудачей или медиапоток может нарушиться.

Это привело нас к конкретной цели: предоставить публичному интернету небольшую фиксированную UDP-поверхность, при этом по-прежнему направляя каждый пакет в тот трансивер, который владеет соответствующим сеансом WebRTC.

Сравнение архитектур WebRTC для работы с медиаданными

Мы рассмотрели несколько способов добиться этого, включая использование TURN (Traversal Using Relays around NAT) — протокола, при котором периферийный ретранслятор терминирует клиентские аллокации и пересылает трафик от их имени.2

Подход

Плюсы

Минусы

Уникальный IP:port для каждого сеанса (также известен как нативный прямой UDP)

Прямой медиапуть от клиента к серверу

Отсутствие слоя пересылки на пути данных

Требуется один публичный UDP-порт на каждый сеанс

Большие диапазоны портов трудно публиковать и защищать

Плохо подходит для Kubernetes и облачных балансировщиков нагрузки

Уникальный IP:port для каждого сервера

Значительно меньшая публичная UDP-поверхность, чем при выделении порта на сеанс

Один общий сокет на сервер может демультиплексировать множество сеансов

Хорошо работает на одном хосте, но само по себе не подходит для общего балансируемого флота

Демультиплексирование сеансов на одном хосте помогает только после того, как пакет достиг этого хоста; в балансируемом флоте первый пакет все равно может попасть не на тот экземпляр, поэтому по-прежнему нужен детерминированный способ направлять каждый сеанс в процесс, который им владеет


TURN-ретранслятор (с терминацией протокола)

Клиентам достаточно иметь доступ к адресу и порту TURN relay

Можно централизовать политику на периферии

Выделения TURN добавляют дополнительные циклы обмена при настройке

Перемещать или восстанавливать выделения между TURN-серверами по-прежнему сложно

Ретранслятор без сохранения состояния + терминатор с сохранением состояния (связка ретранслятор + трансивер от OpenAI)

Небольшая публичная UDP-поверхность

Трансивер по-прежнему владеет полным сеансом WebRTC

Добавляет один шаг пересылки, прежде чем медиатрафик достигнет целевого трансивера

Требует кастомной координации между ретранслятором и трансивером.

Обзор архитектуры: ретранслятор + трансивер

Архитектура, которую мы внедрили, разделяет маршрутизацию пакетов и завершение протоколов. Сигналинг по-прежнему поступает в трансивер для настройки сеанса, тогда как медиатрафик сначала входит через ретранслятор. Ретранслятор — это легкий уровень пересылки UDP с минимальным публичным присутствием, а трансивер — конечная точка WebRTC с состоянием, находящаяся за ним.

Ретранслятор пересылает пакеты на трансивер без сохранения состояния

Ретранслятор не расшифровывает медиаданные, не выполняет машины состояний ICE и не участвует в согласовании кодеков. Он считывает только тот объем метаданных пакета, который нужен для выбора назначения, а затем пересылает пакет в трансивер, владеющий сеансом. Трансивер по-прежнему видит обычный поток WebRTC и по-прежнему владеет всем состоянием протокола. С точки зрения клиента в сеансе WebRTC ничего не меняется.

Маршрутизация по учетным данным ICE

Маршрутизация первого пакета — ключевой шаг в этой схеме. Ретранслятор должен направить первый пакет от клиента до того, как на самом пути пакета появится какой-либо сеанс, а не останавливать обработку ради внешнего сервиса поиска.

Каждый сеанс WebRTC уже содержит встроенный в протокол механизм маршрутизации: фрагмент имени пользователя ICE, или 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, а не отдельный сокет на сеанс. После того как ретранслятор создает сеанс от исходного IP:port клиента к назначению этого transceiver, последующие пакеты DTLS, RTP и RTCP проходят в рамках сеанса без повторного декодирования ufrag.

Сеанс на ретрансляторе намеренно минимален: он состоит только из сеанса в памяти для пересылки пакетов, а также необходимых счетчиков для мониторинга и таймеров истечения и очистки сеанса. Такой выбор конструкции сохраняет маршрутизацию пакетов непосредственно на пути пакета. Если ретранслятор перезапускается и теряет сеанс, следующий пакет STUN восстанавливает сеанс из подсказки маршрутизации в ufrag. Чтобы сделать это еще надежнее, используется кэш Redis, который хранит сопоставление «IP клиента + порт» и «IP трансивера + порт» после установления маршрута, чтобы его можно было восстановить значительно раньше — до прихода следующего пакета STUN.

Глобальный ретранслятор и сигналинг с гео-маршрутизацией

После того как мы сократили публичную UDP-поверхность до небольшого числа стабильных адресов и портов, мы смогли развернуть одну и ту же схему ретранслятор по всему миру. Глобальный ретранслятор — это наш флот географически распределенных точек входа ретранслятора, и все они реализуют одно и то же поведение пересылки пакетов.

Широкое географическое распределение точек входа сокращает первый переход от клиента к OpenAI, потому что пакет может войти в нашу сеть через ретранслятор, расположенный близко к пользователю как географически, так и с точки зрения сетевой топологии, вместо того чтобы сначала пересекать публичный интернет до удаленного региона. На практике это означает меньшую задержку, меньший джиттер и меньшее число устранимых всплесков потерь до того, как трафик попадет в нашу магистральную сеть.6

Слой Global Relay получает пакеты от клиента и пересылает их кластеру трансиверов

Для сигналинга мы используем географическое и proximity steering от Cloudflare, чтобы начальный HTTP- или WebSocket-запрос попадал в ближайший кластер трансиверов. Контекст запроса определяет местоположение сеанса и то, какая точка входа Глобального ретранслятора объявляется клиенту. SDP-ответ предоставляет адрес Глобального ретранслятора, а ufrag содержит достаточно информации, чтобы Глобальный ретранслятор маршрутизировал медиатрафик в назначенный кластер, а локальный ретранслятор — к целевому трансиверу.

Вместе сигналинг с геонаправлением и Глобальнй ретранслятор направляют и установление, и медиатрафик по близкому входному пути, при этом удерживая сеанс привязанным к одному трансиверу. Это уменьшает время прохождения туда и обратно для сигналинга и для первой проверки связности ICE, что напрямую сокращает время ожидания пользователя до начала речи.

Реализация и производительность ретранслятора

Мы написали сервис ретранслятора на Go и намеренно сохранили его реализацию узкой. В Linux сетевой стек ядра получает UDP-пакеты от сетевого интерфейса машины и доставляет их в сокет — конечную точку операционной системы, из которой процесс читает данные после привязки IP:Port. Ретранслятор работает в пользовательском пространстве, поэтому обычный процесс Go читает заголовки пакетов из этого сокета, обновляет небольшой объем состояния потока и пересылает пакеты без завершения WebRTC. Нам не понадобился никакой фреймворк для обхода ядра, который позволил бы процессу в пользовательском пространстве напрямую опрашивать сетевые очереди для более высокой скорости пакетной обработки, но также добавил бы операционную сложность.

Ключевые проектные решения:

  • Без завершения протоколов: ретранслятор разбирает только заголовки STUN и идентификатор ufrag; для последующих DTLS, RTP и RTCP он использует кэшированное состояние, сохраняя пакеты непрозрачными.
  • Эфемерное состояние: ретранслятор хранит в памяти небольшую таблицу соответствий (с коротким тайм-аутом) между адресами клиентом и трансиверами назначения; это необходимо для управления потоками и обеспечения наблюдаемости.
  • Горизонтальная масштабируемость: множество инстансов ретранслятора работают параллельно за балансировщиком нагрузки. Это состояние не является жестким (критическим) состоянием WebRTC, поэтому перезапуски вызывают минимальные потери трафика и быстрое восстановление потоков.

Меры по повышению эффективности:

  • SO_REUSEPORT — это опция сокета Linux, которая позволяет нескольким воркерам ретранслятора на одной машине привязывать один и тот же UDP-порт. Затем ядро распределяет входящие пакеты между этими воркерами, что позволяет избежать узкого места в одном цикле чтения.
  • runtime.LockOSThread закрепляет каждую goroutine (горутину), читающую UDP, за определенным потоком ОС. В сочетании с SO_REUSEPORT это обычно помогает удерживать пакеты одного и того же потока (исходный и конечный IP:Port плюс протокол) на одном и том же ядре CPU, улучшая локальность кэша и уменьшая переключение контекста.
  • Предварительно выделенные буферы и минимальное копирование снижают накладные расходы на разбор и выделение памяти, чтобы избежать сборки мусора в Go.

Эта реализация справилась с нашим глобальным медиатрафиком реального времени при относительно небольшой инфраструктурной нагрузке ретрансляторов, поэтому мы сохранили более простую конструкцию вместо перехода к обходу ядра.

Результаты и выводы

Эта архитектура позволяет нам запускать медиатрафик WebRTC в Kubernetes, не открывая тысячи UDP-портов. Это важно, потому что меньшая и фиксированная UDP-поверхность проще для защиты и балансировки нагрузки, а также позволяет инфраструктуре масштабироваться без резервирования больших диапазонов публичных портов. Благодаря лучшей поддержке со стороны инфраструктуры Kubernetes и большей безопасности из-за меньшей площади атаки эта конструкция также сохраняет стандартное поведение WebRTC для клиентов и подтверждает, что дизайн без SFU был правильным выбором по умолчанию для нашей нагрузки. Большинство наших сеансов — это чувствительные к задержке соединения точка-точка, которые легче масштабировать, когда сервисам инференса не нужно брать на себя роль полноправных WebRTC-пиров.

Более общий вывод состоит в том, что сложность лучше добавлять в тонкий слой маршрутизации, а не в каждый бэкенд-сервис и не в нестандартное поведение клиента. Кодирование метаданных маршрутизации в поле, уже предусмотренное протоколом, дало нам детерминированную маршрутизацию первого пакета, небольшую публичную UDP-поверхность и достаточную гибкость, чтобы размещать точки входа близко к пользователям по всему миру.

Несколько решений были особенно важны:

  • Сохранять семантику протокола на периферии. Клиенты по-прежнему используют стандартный WebRTC, что сохраняет совместимость браузеров и мобильных приложений.
  • Хранить сложное состояние сеанса в одном месте. Трансивер владеет ICE, DTLS, SRTP и жизненным циклом сеанса; ретранслятор же только пересылает пакеты.
  • Маршрутизировать на основе информации, уже присутствующей при настройке. ICE ufrag дал нам механизм маршрутизации первого пакета без добавления зависимости от поиска на горячем пути.
  • Оптимизировать под типичный случай, прежде чем переходить к обходу ядра. Узкая реализация на Go с аккуратным использованием SO_REUSEPORT, закрепления потоков и разбора с низким числом выделений памяти оказалась достаточной для нашей нагрузки.

Голосовой ИИ в реальном времени работает только тогда, когда инфраструктура делает задержку незаметной. Для нас это означало изменить форму развертывания WebRTC, не меняя при этом того, чего клиенты ожидают от самого WebRTC.