AI Pulse

OpenAI如何构建大规模低延迟语音AI

OpenAI如何构建大规模低延迟语音AI

只有当对话以语音的速度进行时,语音AI才会感觉自然。当网络介入时,人们会立即感受到尴尬的停顿、被截断的打断或延迟的插话。这对ChatGPT语音、使用Realtime API的开发者、在交互式工作流中工作的代理,以及需要在用户说话时处理音频的模型来说都很重要。在OpenAI的规模下,这转化为三个具体要求: - 全球覆盖超过9亿周活跃用户 - 快速连接建立,使用户在会话开始时就能立即说话 - 低且稳定的媒体往返时间,低抖动和低丢包率,使轮流发言感觉干脆

负责实时AI交互的OpenAI团队最近重新架构了我们的WebRTC堆栈,以解决在大规模下开始冲突的三个约束:每会话单端口的媒体终止不适合OpenAI基础设施,有状态的ICE(交互式连接建立)和DTLS(数据报传输层安全)会话需要稳定的所有权,以及全球路由必须保持第一跳的低延迟。在这篇文章中,我们将介绍我们构建的分离中继加收发器架构,该架构为客户端保留了标准的WebRTC行为,同时改变了OpenAI内部路由数据包的方式。

WebRTC是一个开放标准,用于在浏览器、移动应用和服务器之间发送低延迟音频、视频和数据。它通常与点对点通话相关,但它也是客户端到服务器实时系统的实用基础,因为它标准化了交互式媒体的难点:用于连接建立和NAT(网络地址转换)穿越的ICE,用于加密传输的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的——一个用户与一个模型对话,或一个应用程序与一个实时代理对话——每一次轮流都有延迟敏感性。对于这种流量形状,我们选择了收发器模型:一个WebRTC边缘服务终止客户端连接,然后将媒体和事件转换为更简单的内部协议,用于模型推理、转录、语音生成、工具使用和编排。

在这个设计中,收发器是唯一拥有WebRTC会话状态的服务,包括ICE连接检查、DTLS握手、SRTP加密密钥和会话生命周期。这里的“终止”意味着收发器是完成这些握手并加密或解密媒体的端点。将这种状态保持在一个地方使得会话所有权更容易推理,并且让后端服务像普通服务一样扩展,而不是自身作为WebRTC对等端。

在选择收发器模型后,我们的第一个实现是一个基于Pion的单一Go服务,处理信令和媒体终止。它为ChatGPT语音、Realtime API的WebRTC端点以及许多研究项目提供支持。

在操作上,收发器服务做两件事: - 信令:SDP协商、编解码器选择、ICE凭据和会话设置 - 媒体:终止下游WebRTC连接并维护与后端服务的上游连接,用于推理和编排

我们想让这个服务像其他基础设施一样运行:在Kubernetes上,工作负载可以上下扩展,并根据需求变化在主机之间移动。但是传统的每会话单端口WebRTC模型不太适合这种环境,因为它依赖于大型公共UDP端口范围,这些范围在Pod添加、删除或重新调度时难以暴露、保护和控制。

第一个问题是每会话单端口模型本身。在高并发下,这意味着暴露和管理非常大的UDP端口范围。 - 云负载均衡器和Kubernetes服务并非为每个服务有数万个公共UDP端口而设计。每个额外的范围都会增加负载均衡器配置、健康检查、防火墙策略和发布安全方面的操作复杂性。 - 大的UDP端口范围很难保护,因为它们扩大了外部可达的表面积,并使网络策略更难审计。 - 它们也不适合自动扩展。Kubernetes中的Pod经常被添加、删除和重新调度。要求每个Pod保留并通告一个大的稳定端口范围会使这种弹性变得脆弱。 这就是为什么许多WebRTC系统转向每个服务器单个UDP端口,并在该端口后面进行应用级多路复用。

单端口每服务器的设计解决了端口数量问题,但它们引入了第二个问题:在舰队中保持每个会话的所有权。 ICE和DTLS是有状态的协议。创建会话的进程需要继续接收该会话的数据包,以便验证连接检查、完成DTLS握手、解密SRTP以及处理后续的会话更改,如ICE重启。如果同一会话的数据包落在不同的进程上,则设置可能失败或媒体可能会中断。

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

我们评估了几种实现方法,包括TURN(围绕NAT使用中继),其中边缘中继终止客户端分配并代表它们转发流量。

| 方法 | 优点 | 缺点 | |------|------|------| | 唯一IP:端口每会话(也称为本地直接UDP) | 直接的客户端到服务器媒体路径;数据路径中无转发层 | 每个会话需要一个公共UDP端口;大的端口范围难以暴露和保护;不适合Kubernetes和云负载均衡器 | | 唯一IP:端口每服务器 | 比每会话暴露小得多的公共UDP占用;每个服务器一个共享套接字可以多路复用许多会话 | 仅在单个主机上工作良好,但在共享负载均衡的舰队中不行;单主机上的会话多路复用仅在一个数据包到达该主机后才起作用;在负载均衡的舰队中,第一个数据包仍可能落在错误的实例上,因此仍需要确定性的方法将每个会话导向拥有它的进程 | | TURN中继(协议终止) | 客户端只需到达TURN中继地址和端口;可以在边缘集中化策略 | TURN分配增加了设置往返;跨TURN服务器移动或恢复分配仍然困难 | | 无状态转发器 + 有状态终止器(OpenAI的中继 + 收发器) | 小的公共UDP占用;收发器仍拥有完整的WebRTC会话 | 在媒体到达拥有它的收发器之前增加了一跳转发;需要中继和收发器之间的自定义协调 |

我们发布的架构将数据包路由与协议终止分开。信令仍到达收发器进行会话设置,而媒体首先通过中继进入。中继是一个轻量级的UDP转发层,具有小的公共占用,收发器是其后端有状态的WebRTC端点。 中继不解密媒体、运行ICE状态机或参与编解码器协商。它读取足够的数据包元数据来选择目的地,然后将数据包转发给拥有该会话的收发器。收发器仍然看到正常的WebRTC流,并且仍然拥有所有协议状态。从客户端的角度来看,关于WebRTC会话没有任何改变。

第一个数据包路由是这个设置中的关键步骤。中继必须在任何会话存在于数据包路径上之前,而不是暂停在外部查找服务上,路由来自客户端的第一个数据包。 每个WebRTC会话已经携带了一个协议本机路由钩子:ICE用户名片段(ufrag),这是在会话设置期间交换并在STUN连接检查中回显的短标识符。我们生成服务器端ufrag,使其包含足够的路由元数据,以便中继推断目标集群和拥有它的收发器。

在信令期间,收发器分配会话状态,并在SDP应答中返回一个共享的中继VIP和UDP端口。VIP是前端中继舰队的虚拟IP地址;与端口结合,它给客户端一个稳定的单一目的地,例如`203.0.113.10:3478`,即使后面有许多中继实例。客户端的第一个媒体路径数据包通常是STUN(NAT会话穿越工具)绑定请求,ICE使用它来验证数据包是否可以到达通告的地址。 中继解析第一个STUN数据包,只读取服务器ufrag,解码路由提示,并将数据包转发给拥有它的收发器。每个收发器监听一个共享的UDP套接字,这意味着一个绑定到内部IP:端口的操作系统端点,而不是每个会话一个套接字。在中继从客户端源IP:端口到该收发器目的地创建会话后,后续的DTLS、RTP和RTCP数据包会在会话内流动,而无需重新解码ufrag。

中继的会话是有意最小化的,只包含一个内存中的会话以指导数据包转发,以及用于监控的必要计数器和用于会话过期和清理的定时器。这种设计选择直接将数据包路由保持在数据包路径上。如果中继重启并丢失了会话,下一个STUN数据包会从ufrag路由提示重建会话。为了使其更加可靠,采用Redis缓存来保存<客户端IP+端口,收发器IP+端口>的映射,一旦路由建立,就可以在下一个STUN数据包到达之前更早地恢复它。

一旦我们将公共UDP表面减少到少量稳定的地址和端口,我们就可以在全球范围内部署相同的中继模式。Global Relay是我们的地理分布中继入口点舰队,它们都实现相同的数据包转发行为。 广泛的地理入口缩短了客户端到OpenAI的第一跳,因为数据包可以在靠近用户(在地理和网络拓扑上)的中继进入我们的网络,而不是首先穿越公共互联网到达远程区域。实际上,这意味着更低的延迟、更少的抖动以及更少的在流量到达我们骨干网之前的可避免的丢失突发。

我们使用Cloudflare地理和邻近度引导进行信令,以便初始HTTP或WebSocket请求到达附近的收发器集群。请求上下文决定了会话的位置以及向客户端通告哪个Global Relay入口点。SDP应答提供Global Relay地址,而ufrag包含足够的信息供Global Relay将媒体路由到指定集群,并由中继路由到目标收发器。

地理引导的信令和Global Relay一起将设置和媒体都放在附近的入口路径上,同时将会话锚定到一个收发器。这减少了信令和首次ICE连接检查的往返时间,直接缩短了用户开始说话之前的等待时间。

我们用Go编写了中继服务,并有意保持实现范围狭窄。在Linux上,内核的网络栈从机器网络接口接收UDP数据包,并将它们交付给套接字,即进程在绑定一个IP:端口后读取的操作系统端点。中继运行在用户空间,因此常规的Go进程从该套接字读取数据包头,更新少量流状态,并在不终止WebRTC的情况下转发数据包。我们不需要任何内核旁路框架,它可以让用户空间进程直接轮询网络队列以实现更高的数据包速率,但也会增加操作复杂性。

关键设计选择: - 无协议终止:中继只解析STUN头/ufrag;它对后续的DTLS、RTP和RTCP使用缓存状态,保持数据包不透明。 - 瞬时状态:它维护一个小的、短超时的内存映射,将客户端地址映射到收发器目的地,用于流状态和可观测性。 - 水平可扩展性:多个中继实例并行运行在负载均衡器后面。状态不是稳定的WebRTC状态,因此重启导致最小的流量下降和快速的流恢复。 - 效率措施: - SO_REUSEPORT是一个Linux套接字选项,允许同一台机器上的多个中继工作者绑定同一个UDP端口。然后内核将传入的数据包分布在这些工作者之间,避免了单个读取循环的瓶颈。 - runtime.LockOSThread将每个UDP读取的goroutine固定到特定的OS线程。与SO_REUSEPORT结合,这倾向于将来自同一流(源和目的IP:端口加协议)的数据包保留在同一CPU核心上,改善缓存局部性并减少上下文切换。 - 预分配的缓冲区和最小复制使解析和分配开销保持较低,以避免Go中的垃圾收集。

这种实现以相对较小的中继占用处理了我们的全球实时媒体流量,因此我们保留了更简单的设计,而不是采用内核旁路线。

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

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

有几个选择尤其重要: - 在边缘保留协议语义。客户端仍然说标准的WebRTC,这保持了浏览器和移动互操作性。 - 将硬的会话状态保持在一个地方。收发器拥有ICE、DTLS、SRTP和会话生命周期;中继只转发数据包。 - 在设置中已经存在的信息上进行路由。ICE ufrag给了我们一个第一数据包路由钩子,而无需添加热路径查找依赖。 - 在求助于内核旁路之前优化常见情况。一个狭窄的Go实现,加上仔细使用SO_REUSEPORT、线程固定和低分配解析,足够支持我们的工作负载。

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

📎 阅读原文 · OpenAI

📬 订阅 AI Pulse

每天两次更新,不错过重要信号

▲ 回到顶部