只有当对话节奏逼近 人类说话的自然节拍 时,语音 AI 才会「好听、好用」。网络一旦在中间插一脚,人类会立刻听成:尴尬停顿、抢话不完整、打断(barge-in)变慢。这对 ChatGPT 语音模式、使用 Realtime API 的开发者、交互式 Agent 流水线,以及「边听边推理」的模型形式都成立。
在 OpenAI 自称的体量下,问题被压成三条 硬指标:
- 全球可达:服务 9 亿 + 周活 级别的用户分布;
- 连接建立要快:会话一开始就能说话,而不是等一串握手转圈;
- 媒体路径 RTT 低且稳:抖动、丢包可控,话轮切换(turn-taking)才显得干脆。
负责实时交互的团队近期 重做了 WebRTC 栈,用来缓解三件事在规模上来以后开始「互相打架」的约束:
- 「每会话独占一个公网 UDP 端口」的媒体终结模型,和 OpenAI 的基础设施形态 并不合拍;
- 有状态的 ICE / DTLS 会话 需要 稳定 ownership(归属进程);
- 全球路由 必须把 首跳延迟 压住。
下文整理原文脉络:在客户端仍保持标准 WebRTC 语义 的前提下,分机房内部如何改包的路由形态——也就是他们所称的 split relay + transceiver(拆分中继 + 收发器)架构。
为什么实时语音产品会押注 WebRTC?
WebRTC 是面向浏览器、移动 App 与服务器之间 低延迟音视频与数据 的开放标准。很多人把它和 P2P 通话绑在一起,但它同样是 客户端—服务器实时系统 的务实底座,因为把最难的几块「协议事实标准」封好了:
- ICE:建连与 NAT 穿透;
- DTLS + SRTP:加密传输;
- 编解码协商:发送端压缩、接收端解压;
- RTCP:质量控制与反馈;
- 以及浏览器侧的 回声消除、抖动缓冲 等能力。
对 AI 产品来说,关键性质是:音频以连续流到达。语音 Agent 可以在用户 尚未说完 时就开始 转写、推理、调工具或合成回复——这是 真·对话感 与 对讲机式 push-to-talk 的分水岭。
OpenAI 也点名了生态里的基础工作:例如 Justin Uberti(WebRTC 早期架构师之一)与 Pion 作者 Sean DuBois 等;两人现均在 OpenAI 任职——团队得以在 久经沙场的媒体栈 上继续往前推,而不是从零发明低层传输、加密与拥塞控制。
媒体架构选型:SFU vs Transceiver(收发器)
在哪里终结 WebRTC(由谁持有连接、解密媒体、承担会话状态)决定了延迟、路由、故障域与扩展方式。
方案一:SFU(选择性转发单元)
SFU 从每个参与者收一路 WebRTC,再 选择性转发 给其他参与者;每个参与者各有一条独立 WebRTC 连接,AI 也可以作为 又一个参与者 入会。适合 天然多人 场景:群组通话、教室、协作会议;编解码、RTCP、数据通道、录制、按流策略都能 集中在一处。
即便在「人对模型」产品里,SFU 也常常是默认起点:一套系统同时覆盖 信令、媒体路由、观测与未来扩展(例如人工接管、再加参与者)。
方案二:Transceiver——边缘终结 WebRTC,后端走「更简单」的协议
OpenAI 的主体流量形态是 1:1:一个人对一只模型,或一个应用对一只实时 Agent,每一轮都对延迟极端敏感。
他们选择 transceiver 模型:WebRTC 边缘服务 在边缘 终结客户端连接,再把媒体与事件 转换成内部更简单的协议,对接推理、转写、语音合成、工具与编排。
在此设计里,只有 transceiver 拥有完整 WebRTC 会话状态:ICE 连通性检查、DTLS 握手、SRTP 密钥、会话生命周期。「终结」意味着 握手与加解密媒体 都在 transceiver 完成;后端服务 不必再扮演 WebRTC 对端,可以像普通微服务一样扩缩。
核心矛盾:WebRTC 遇上 Kubernetes
第一版实现是 单个 Go 服务(基于 Pion)同时扛 信令 + 媒体终结,支撑 ChatGPT 语音、Realtime API 的 WebRTC 入口以及若干研究项目。
从职责上 transceiver 做两件事:
- 信令:SDP 协商、编解码选择、ICE 凭据、会话搭建;
- 媒体:对下终结用户 WebRTC;对上维护连到推理/编排后端的连接。
团队希望它像其它服务一样跑在 Kubernetes 上——随负载扩缩、在节点间迁移。但「每会话一个 UDP 端口」的传统 WebRTC 部署方式,在这里会集体踩雷。
端口耗尽与运维面
高并发下要对外暴露、管理 巨大范围的公网 UDP 端口。
- 云负载均衡与 K8s Service 不是为「单服务上万 UDP 端口」设计的;每加一段端口区间,都在 LB 配置、健康检查、防火墙、发布安全 上加成本。
- 大端口面 = 更大攻击面,网络策略审计也更痛。
- 与弹性扩缩天然冲突:Pod 频繁增删、重调度时,还要求每个 Pod 稳定持有并宣告一大段端口,弹性会变得 脆弱。
这也是许多 WebRTC 系统最终走向「单台服务器一个 UDP 端口 + 应用层多路复用」的原因。
状态粘性(stickiness)
单端口/多路复用缓解端口问题,但带来第二个问题:会话归属。
ICE 与 DTLS 是有状态协议。创建会话的进程必须持续收到该会话的数据包,才能完成连通性检查、DTLS、SRTP 解密,以及后续 ICE restart 等变更。若同一会话的包落到 另一个进程,建连失败或媒体直接断。
目标因此非常具体:对外只暴露 少量、固定 的 UDP 入口,同时 把每个包 Deterministic 地送到拥有该 WebRTC 会话的 transceiver。
架构路线对比
路线 | 优点 | 缺点 |
每会话独立公网 IP:端口(直连 UDP) | 媒体路径直接;数据面无转发层 | 每会话一个公网 UDP 端口;大端口区间难暴露与加固;与 K8s / 云 LB 契合度差 |
每服务器唯一 IP:端口 | 公网 UDP 面远小于「每会话一端口」;单进程可共享 socket 多路复用 | 单机内好做;跨 负载均衡集群 时首包仍可能落到「错误实例」,仍需 确定性 steering |
TURN 中继(协议终结型) | 客户端只需打到一个中继地址;策略可集中在边缘 | TURN allocation 增加建连 RTT;跨 TURN 迁移/恢复仍难 |
无状态转发层 + 有状态终结点(OpenAI:Relay + Transceiver) | 公网 UDP 面小;transceiver 仍完整持有 WebRTC 状态 | 媒体多一跳转发;relay 与 transceiver 间的协作需自研 |
Relay + Transceiver:把「路由」与「终结」拆开
上线的架构 分离了包路由与协议终结:
- 信令 仍打到 transceiver 完成会话搭建;
- 媒体 先进入 Relay(中继),再由中继转发到对的 transceiver。
Relay 是轻量 UDP 转发层,对外 UDP 面很小;transceiver 是躲在后面的 有状态 WebRTC 终结点。
Relay 只做转发,不终结 WebRTC
Relay 不解密媒体、不跑 ICE 状态机、不参与编解码协商;只读取 够用即可 的元数据来选择目的地,然后把包交给 持有会话的 transceiver。对客户端而言,WebRTC 会话语义不变。
用 ICE 凭据(ufrag)做首包路由
难点是:中继必须在「路径上尚无完整会话上下文」之前,就能路由客户端的第一帧有效包——而不是先卡去查中心化服务。
WebRTC 里现成的挂钩是 ICE username fragment(ufrag):建连时在信令里交换,并会出现在 STUN 连通性检测报文中。OpenAI 生成服务端 ufrag,让其中编入 刚刚好 的提示信息,中继据此推断 目标集群与所属 transceiver。
流程概要:
- 信令阶段,transceiver 分配会话状态,并在 SDP Answer 里返回 共享的 Relay VIP + UDP 端口(VIP 是中继集群前的虚拟地址,形如
203.0.113.10:3478,背后是多实例)。 - 客户端第一条走媒体路径的数据往往是 STUN Binding Request,用于 ICE 校验可达性。
- Relay 只解析这层 STUN,读出 服务器侧 ufrag,解码路由提示,把包转给 owner transceiver。
- 各 transceiver 监听 共享 UDP socket(一个 OS 级 IP:Port 端点,不是每会话一个 socket)。Relay 为「客户端源 IP:端口 → transceiver 目的地」建 极简转发会话;后续 DTLS / RTP / RTCP 在同一 flow 上继续走,无需每包重解 ufrag。
Relay 的状态刻意保持 短命、内存内:转发所需的最小 map、监控计数、过期清理定时器。若 Relay 重启丢状态,下一帧 STUN 仍可凭 ufrag 重建路由。为更稳,文中还提到用 Redis 缓存 〈客户端 IP+ 端口,transceiver IP+ 端口〉 映射,在下一帧 STUN 到来前 就有机会恢复路径。
Global Relay 与地理亲和信令
把公网 UDP 收敛到少量稳定地址后,同一套 Relay 模式 可以 全球复制:Global Relay = 地理上分散的中继入站点,行为一致。
地缘入站 缩短 用户 → OpenAI 的第一跳,在地理与运营商拓扑上都更「就近」,通常带来 更低 RTT、更少抖动与可避免丢包突刺,再进入骨干网。
信令侧,他们使用 Cloudflare 的地理与就近 steering,让首次 HTTP / WebSocket 到达 附近的 transceiver 集群;请求上下文决定 会话落点 以及向客户端公布的 Global Relay 入站点。SDP Answer 给 Global Relay 地址,ufrag 再携带 足够信息 让 Global Relay 把媒体送到指定集群、并由 relay 送到具体 transceiver。
地理亲和信令 + Global Relay 同时压缩 信令 RTT 与 首包 ICE 检测 RTT,直接缩短 从点「开始说话」到媒体真正跑起来 的等待。
Relay 的实现与性能取向
Relay 用 Go 编写,刻意保持 窄职责。Linux 内核把网卡上的 UDP 交给 socket;Relay 在用户态读包头、维护少量流状态、转发 不落地终结 WebRTC。团队表示 未上 kernel bypass(如 DPDK 一类)——那能换更高 PPS,但运维复杂度也上去,对他们 当前流量形态 不划算。
几个关键工程点(原文要点):
- 不在热路径做协议终结:除首包/必要 STUN 外,DTLS/RTP/RTCP 保持 opaque(不透明转发);
- 短命内存态:小 map + 短超时;水平扩展多实例挂 LB;重启影响面有限、流可快速自愈;
- SO_REUSEPORT:多 worker 同绑一个 UDP 端口,由内核把包扇到不同 worker,避免单读循环瓶颈;
- runtime.LockOSThread:把读 UDP 的 goroutine 钉在固定 OS 线程,配合
SO_REUSEPORT尽量让同一 五元组流 落在 同一 CPU 核,改善缓存局部性、减少上下文切换; - 预分配缓冲、少拷贝、少分配:降低 Go GC 压力。
结果与经验总结
该架构让 OpenAI 能在 Kubernetes 上跑 WebRTC 媒体,而 不必对公网撒成千上万 UDP 端口:收敛 UDP 面 带来 更好负载均衡与更安全 的边界,也让基础设施 按常规云原生方式扩缩。
同时,客户端仍是标准 WebRTC,浏览器与移动生态的互操作性得以保留;团队认为对其 点对点、延迟敏感、后端不应扮演 WebRTC peer 的 workload,默认不走 SFU 是正确默认。
文中提炼的几条「更要紧的选择」:
- 把复杂度加在薄路由层,而不是摊进每个后端服务,也不要求客户端魔改协议。
- 边缘保持协议语义;硬状态只留在一处(transceiver 握 ICE/DTLS/SRTP/生命周期)。
- 路由信息来自建连阶段已有的字段(ICE ufrag),避免热路径依赖额外 lookup。
- 先把常见路径优化到极致,再考虑 kernel bypass。