乐于分享
好东西不私藏

OpenAI:如何在大规模场景下实现低延迟语音AI

OpenAI:如何在大规模场景下实现低延迟语音AI

How OpenAI delivers low-latency voice AI at scale

第一部分:技术点总结

第二部分:原文翻译

第三部分:适用场景分析

技术点一:Relay + Transceiver 架构模式

解决的问题:WebRTC传统架构与Kubernetes云原生环境的兼容性冲突

实现方式:
– 将数据包路由(Relay)与协议终止(Transceiver)分离
– Relay作为轻量级UDP转发层,只负责数据包路由,不参与任何协议逻辑
– Transceiver作为有状态WebRTC端点,拥有完整的会话状态(ICE、DTLS、SRTP)

为什么这样设计:

– 解决Kubernetes环境下Pod频繁调度导致的端口管理难题
– 将复杂的WebRTC状态保持在单一服务中,简化后端服务的扩展逻辑
– 保持客户端标准WebRTC行为不变,不破坏互操作性

技术点二:ICE ufrag 内嵌路由元数据

解决的问题:首包路由的确定性难题

实现方式:

– 在会话建立时,Transceiver生成包含路由元数据的ufrag
– ufrag编码了目标集群和Transceiver信息
– Relay只需解析STUN头即可获取路由信息,无需外部查询

为什么这样设计:

– 利用WebRTC协议中原有的字段,无需添加额外的协议扩展
– 首包路由在数据包路径上完成,无额外延迟和依赖
– 当Relay重启时,下一个STUN包即可重建路由状态

技术点三:Global Relay 地理分布式架构

解决的问题:全球用户的首跳延迟问题

实现方式:
– 在全球多个地理区域部署Relay集群
– 使用Cloudflare进行地理和接近度导向
– 用户请求被路由到最近的Relay入口点

为什么这样设计:

– 缩短用户到OpenAI网络的物理距离
– 减少公共互联网跨区域传输带来的抖动和丢包
– 保持会话锚定在特定Transceiver,同时让入口更靠近用户

技术点四:Redis加速的会话恢复机制

解决的问题:Relay重启时的快速故障恢复

实现方式:

– 在内存会话之外,增加Redis缓存层
– 缓存<客户端IP+端口, Transceiver IP+端口>映射
– Relay重启后可立即从Redis恢复路由状态

为什么这样设计:

– 减少Relay故障对用户体验的影响
– 在下一个STUN包到达之前就能恢复路由
– 提供额外的数据持久性保障

技术点五:SO_REUSEPORT + Goroutine线程固定

解决的问题:高并发UDP处理性能

实现方式:

– 使用SO_REUSEPORT允许多worker绑定同一端口
– 使用runtime.LockOSThread固定goroutine到CPU核心
– 预分配缓冲区,最小化内存分配和GC压力

为什么这样设计:

– 避免单读取线程的性能瓶颈
– 改善CPU缓存局部性
– 在用户空间实现高效的数据包处理,无需内核旁路

技术点六:Transceiver优先于SFU的架构选择

解决的问题:1:1实时交互场景下的架构效率

实现方式:

– 对于大多数1:1会话场景,选择Transceiver模型而非SFU
– Transceiver直接连接推理后端,无需媒体服务器的中间转发
– 后端服务作为普通服务扩展,无需理解WebRTC协议

为什么这样设计:

– 减少媒体转发的延迟开销
– 降低架构复杂度,避免引入不必要的中介层
– 让推理和编排逻辑更清晰,与WebRTC解耦

—-第二部分-原文翻译—-

摘要

  语音AI只有在对话以人类自然语速进行时,才会让用户感到”自然”。当网络成为瓶颈时,用户会立即感受到尴尬停顿、语言被截断、或迟滞的打断插入。这对ChatGPT语音功能、使用Realtime API构建应用的开发者、在交互式工作流中运行的AI Agent,以及需要在用户仍在说话时就处理音频的模型都至关重要。

在OpenAI的规模下,这转化为三个具体需求:

全球覆盖:服务超过9亿周活跃用户

快速连接建立:用户能够在会话开始时立即开始说话

低且稳定的媒体往返时间:低抖动和低丢包,使对话轮转感觉干脆利落

   负责实时AI交互的OpenAI团队最近重构了其WebRTC技术栈,以解决三个在大规模时开始相互冲突的约束:每个会话一个端口的媒体终止模式与OpenAI基础设施的契合度不佳、有状态的ICE和DTLS会话需要稳定的所有权归属、全球路由必须保持首跳延迟较低。

在本文中,我们将深入介绍我们构建的分离式Relay(转发)+ Transceiver(收发器)架构,该架构在保持客户端标准WebRTC行为的同时,改变了OpenAI内部基础设施中的数据包路由方式。

WebRTC让我们能够构建实时AI产品

WebRTC是一项开放标准,用于在浏览器、移动应用和服务器之间发送低延迟的音频、视频和数据。它通常与点对点通话相关联,但实际上也是客户端到服务器实时系统的一个实用基础,因为它将交互式媒体中困难的部分进行了标准化:ICE(交互式连接建立)用于连接建立和NAT穿透、DTLS和SRTP用于加密传输、编解码器协商用于音频压缩和解压、RTCP用于质量控制,以及客户端特性如回声消除和抖动缓冲。

这种标准化对AI产品至关重要。如果没有WebRTC,每个客户端都需要针对如何跨NAT建立连接、如何加密媒体、如何协商编解码器以及如何适应不断变化的网络条件等问题给出不同的答案。有了WebRTC,我们可以基于一个已在浏览器和移动平台上实现的协议栈来构建,专注于将实时媒体连接到模型的基础设施本身。

我们也基于WebRTC生态系统本身来构建,包括成熟的开源实现和保持浏览器、移动应用和服务器之间互操作性的标准化工作。Justin Uberti(WebRTC创始人之一)和Sean DuBois(Pion的创建者和维护者)的基础性工作,使得我们这样的团队能够基于久经考验的媒体基础设施进行构建,而无需重新发明底层传输、加密和拥塞控制行为。我们很幸运,Justin和Sean现在都是OpenAI的同事,帮助指导我们如何将WebRTC和实时AI更紧密地结合在一起。

对于AI来说,最重要的特性是音频能够以连续流的形式到达。说话代理可以在用户仍在说话时就开始转录、推理、调用工具或生成语音,而无需等待完整上传。这就是让系统感觉像”对话”而不是”按下说话”的区别。

选择媒体架构

一旦选择了WebRTC,下一个问题就是在哪里终止它(我们将在哪里接收并拥有WebRTC连接——例如,在边缘),以及如何将这些会话连接到推理后端。终止点的选择很重要,因为它决定了我们如何处理实时会话状态、媒体传输、路由、延迟和故障隔离。

SFU(选择性转发单元) 是一种媒体服务器,它从每个参与者接收一个WebRTC流,并选择性地将流转发给其他参与者。在这种模型中,SFU为每个参与者终止一个独立的WebRTC连接,而AI作为会话中的另一个参与者加入。这对于本质上是多方参与的产品(如群组通话、教室或协作会议)可能是很好的选择。它将音频编解码器、RTCP消息、数据通道、录制和每个流的策略集中在一处。

即使在客户端到AI的产品中,SFU通常也是默认的起点,因为它让团队能够复用一套成熟的系统来处理信令、媒体路由、录制、可观测性以及未来扩展(如人工接管或添加更多参与者)。

我们的工作负载不同。大多数会话是1:1的——一个用户与一个模型对话,或者一个应用与一个实时Agent对话——而且每次轮转都有延迟敏感性。对于这种流量模式,我们选择了Transceiver(收发器)模型:一个WebRTC边缘服务终止客户端连接,然后将媒体和事件转换为用于模型推理、转录、语音生成、工具调用和编排的更简单的内部协议。

在这种设计中,Transceiver是唯一拥有WebRTC会话状态的服务,包括ICE连接检查、DTLS握手、SRTP加密密钥和会话生命周期。这里的”终止”意味着Transceiver是完成这些握手并加密或解密媒体的端点。将状态保持在一处使得会话所有权更容易推理,也让后端服务能够像普通服务一样扩展,而无需充当WebRTC对等端。

核心部署问题:WebRTC遇上Kubernetes

   在选择Transceiver模型后,我们的第一个实现是一个基于Pion构建的单一Go服务,同时处理信令和媒体终止。它支撑着ChatGPT语音、Realtime API的WebRTC端点以及一些研究项目。

从运维角度,Transceiver服务做两件事:

信令:SDP协商、编解码器选择、ICE凭证和会话设置

媒体:终止下游WebRTC连接,并维护与后端服务的上游连接,用于推理和编排

我们希望该服务像其他基础设施一样运行:在Kubernetes上,工作负载可以随时扩缩容,并根据需求变化在主机之间迁移。但传统的每个会话一个端口的WebRTC模型与这种环境不太契合,因为它依赖于大量难以暴露、保护和在Pod添加、移除或重新调度时保持稳定的公共UDP端口范围。

端口耗尽

第一个问题是每个会话一个端口的模型本身。在高并发下,这意味着要暴露和管理非常大的UDP端口范围。

云负载均衡器和Kubernetes服务并非为每个服务数万个公共UDP端口而设计。每个额外的范围都会在负载均衡器配置、健康检查、防火墙策略和发布安全性方面增加运维复杂性。

大型UDP端口范围难以保护,因为它们扩大了外部可访问的表面积,使网络策略更难审计。

它们也不适合自动扩缩容。Pod在Kubernetes中不断被添加、移除和重新调度。要求每个Pod预留并公布一个大型稳定的端口范围,使这种弹性变得脆弱。

这就是为什么许多WebRTC系统转向每个服务器单一UDP端口,并在该端口后面进行应用层解复用的原因。

状态粘性

  每个服务器单一端口的设计解决了端口数量问题,但引入了第二个问题:如何在整个集群中保持每个会话的所有权。

ICE和DTLS是有状态的协议。创建会话的进程需要持续接收该会话的数据包,以便验证连接检查、完成DTLS握手、解密SRTP以及处理后续会话变更(如ICE重启)。如果同一会话的数据包落在不同的进程上,建立可能会失败或媒体可能中断。

这给了我们一个明确的目标:向公共互联网暴露一个小而固定的UDP表面,同时仍将每个数据包路由到拥有相应WebRTC会话的Transceiver。

WebRTC媒体架构对比

  我们评估了多种实现方式,包括TURN(通过中继绕过NAT的遍历),即边缘中继终止客户端分配并代表他们转发流量。

架构概览:Relay + Transceiver

我们交付的架构将数据包路由与协议终止分离。信令仍然到达Transceiver进行会话设置,而媒体首先通过Relay进入。Relay是一个轻量级的UDP转发层,具有小型公共占用;Transceiver是其后方的有状态WebRTC端点。

Relay不解密媒体、不运行ICE状态机、不参与编解码器协商。它只读取足够的数据包元数据来选择目的地,然后将数据包转发到拥有该会话的Transceiver。Transceiver仍然看到正常的WebRTC流程,并且仍然拥有所有协议状态。从客户端的角度来看,WebRTC会话没有任何变化。

基于ICE凭证的路由

  首包路由是此设置中的关键步骤。Relay必须在会话尚不存在于数据包路径上时路由来自客户端的首包,而不是暂停在外部查找服务上。

  每个WebRTC会话已经携带了一个协议原生的路由钩子:ICE用户名片段(ufrag) ,这是一个在会话设置期间交换的短标识符,在STUN连接检查中会被回显。我们生成服务器端ufrag,使其包含足够的路由元数据供Relay推断目标集群和拥有的Transceiver。

在信令期间,Transceiver分配会话状态并在SDP应答中返回共享的Relay VIP和UDP端口。VIP是fronting Relay集群的虚拟IP地址;结合端口,它为客户端提供一个单一稳定的目标,如203.0.113.10:3478,即使许多Relay实例在其后方运行。客户端的首个媒体路径数据包通常是STUN绑定请求,ICE用它来验证数据包是否能够到达公布地址。

Relay解析首个STUN数据包的足够部分以读取服务器ufrag,解码路由提示,并将数据包转发给拥有的Transceiver。每个Transceiver在一个共享UDP套接字上监听,意味着一个绑定到内部IP:Port的操作系统端点,而不是每个会话一个套接字。在Relay从客户端源IP:Port到Transceiver目标创建会话后,后续的DTLS、RTP和RTCP数据包在会话内流动,无需重新解码ufrag。

Relay的会话特意设计为最小化的:仅包含用于指导数据包转发的内存会话,以及用于监控的必要计数器和用于会话过期和清理的计时器。这一设计选择保持数据包路由直接在数据包路径上。如果Relay重启并丢失会话,下一个STUN数据包会从ufrag路由提示重建会话。为了使其更加可靠,我们使用Redis缓存来保存<客户端IP+端口, Transceiver IP+端口>的映射,一旦路由建立就可以提前恢复,而不必等待下一个STUN数据包到达。

全球Relay和地理导向信令 

  一旦我们将公共UDP表面缩减为少量稳定地址和端口,我们就可以在全球范围内部署相同的Relay模式。Global Relay是我们的地理分布式Relay入口点集群,它们都实现相同的包转发行为。

广泛的地理入口缩短了首个客户端到OpenAI的跳数,因为数据包可以在靠近用户的Relay处进入我们的网络,无论是在地理位置还是在网络拓扑上,而不是先跨越公共互联网到达遥远区域。实际上,这意味着更低的延迟、更少的抖动和在流量到达骨干网之前更少的可避免的丢包突发。

我们使用Cloudflare地理和接近度导向来处理信令,使初始HTTP或WebSocket请求到达附近的Transceiver集群。请求上下文决定会话的位置以及向客户端公布哪个Global Relay入口点。SDP应答提供Global Relay地址,而ufrag包含足够的信息供Global Relay将媒体路由到指定集群,并供Relay路由到目标Transceiver。

地理导向信令和Global Relay共同将设置和媒体都置于附近入口路径,同时保持会话锚定在一个Transceiver上。这减少了信令的往返时间和首次ICE连接检查的往返时间,直接缩短了用户等待开始说话的时间。

Relay实现与性能

  我们用Go编写了Relay服务,并有意保持实现精简。在Linux上,内核的网络栈从机器的网络接口接收UDP数据包,并将它们传递到一个套接字,即进程在绑定IP:Port后读取的操作系统端点。Relay运行在用户空间,因此一个普通的Go进程从这个套接字读取数据包头、更新少量流状态,并在不终止WebRTC的情况下转发数据包。我们不需要任何内核旁路框架——它允许用户空间进程直接轮询网络队列以获得更高的数据包速率,但也会增加运维复杂性。

关键设计选择:

无协议终止:Relay只解析STUN头/ufrag;它对后续的DTLS、RTP和RTCP使用缓存状态,保持数据包不透明。

临时状态:它维护一个小型、短超时、内存中的映射表,从客户端地址到Transceiver目的地,用于流状态和可观测性。

水平可扩展性:多个Relay实例在负载均衡器后方并行运行。状态不是硬WebRTC状态,因此重启导致最小的流量下降和快速的流恢复。

效率措施:

– SO_REUSEPORT是一个Linux套接字选项,允许同一台机器上的多个Relay worker绑定同一个UDP端口。然后内核在这些worker之间分配传入的数据包,避免了单一读取循环瓶颈。

– runtime.LockOSThread将每个UDP读取goroutine固定到特定操作系统线程。结合SO_REUSEPORT,这倾向于将同一流的数据包保留在同一个CPU核心上,改善缓存局部性并减少上下文切换。

– 预分配缓冲区和最小化复制保持解析和分配开销低,以避免Go中的垃圾回收。

这个实现以相对较小的Relay占用处理了我们全球实时媒体流量,所以我们保持了这个更简单的设计,而不是走上内核旁路的路子。

结果与经验

      这个架构让我们能够在Kubernetes中运行WebRTC媒体,而无需暴露数万个UDP端口。这很重要,因为更小且固定的UDP表面更容易保护和使用负载均衡,而且它让基础设施能够扩展,而无需预留大型公共端口范围。由于Kubernetes更好的基础设施支持和更小表面积带来的更高安全性,这个设计也保持了客户端的标准WebRTC行为,并确认SFU-less设计对我们的工作负载是正确的默认选择。我们的大多数会话是点对点的、延迟敏感的,当推理服务不需要像WebRTC对等端那样运行时,扩展起来更容易。

更广泛的教训是:添加复杂性的最佳位置是在一个薄的路由层中,而不是在每个后端服务中,也不是在自定义客户端行为中。将路由元数据编码到协议原生字段中给了我们确定性的首包路由、小的公共UDP占用,以及足够的灵活性将入口放在全球用户附近。

几个选择特别重要:

在边缘保持协议语义。客户端仍然使用标准WebRTC,这保持了浏览器和移动互操作性完整。

将硬会话状态保持在一处。Transceiver拥有ICE、DTLS、SRTP和会话生命周期;Relay只转发数据包。

基于设置中已有的信息进行路由。ICE ufrag给了我们首包路由钩子,而无需添加热路径查找依赖。

在触及内核旁路之前先优化常见情况。一个精窄的Go实现,谨慎使用SO_REUSEPORT、线程固定和低分配解析,对我们的工作负载已经足够。

实时语音AI只有在基础设施让延迟感觉”隐形”时才有效。对我们来说,这意味着改变WebRTC部署的形态,而不改变客户端对WebRTC本身的期望。

第三部分:适用场景分析

OpenAI的这套架构最适合以下场景:
1. 1:1实时语音交互:用户与单个AI模型的对话,用户体验最优先
2. 极低延迟敏感:对首包延迟、轮转延迟有极致要求
3. 规模可预测:当前规模已经很大,但增长相对平稳
4. 快速迭代:需要快速上线、快速验证的产品阶段

不太适合以下场景:
5. 多方视频会议:需要SFU的流转发和混流能力
6. 内容录制和回放:缺少集中的媒体处理节点
7. 需要深度媒体分析:如实时质量监控、内容审核(虽然可以旁路处理)
8. 混合场景:语音、视频、屏幕共享混合的场景