Appearance
Realtime API最牛逼的是延迟:从你对着手机说一句话开始,到听到AI返回声音,只需要不到0.3秒。
架构概览
OpenAI没有用行业默认方案,自己设计了relay + transceiver两层架构:
| 层级 | 职责 | 特点 |
|---|---|---|
| relay | 只负责转发数据包 | 极轻量,不解密、不解码、不参与协商 |
| transceiver | 负责所有通话状态 | ICE、DTLS、SRTP、会话生命周期 |
为什么不用SFU
做WebRTC服务,行业默认选择是SFU(选择性转发单元)——一个中转站,每个参与者跟它建一条连接。
但OpenAI的场景不一样:
- 绝大多数会话是1:1,一个用户对一个模型
- 每轮对话都对延迟极度敏感
- SFU的多方通话基础设施是多余的
他们还评估过TURN(穿透防火墙中继),但TURN要求中继节点持有客户端连接分配状态,不够轻量。
最后选了transceiver模型:在网络边缘终止WebRTC,转换为更简单的内部协议。
relay的极简设计
relay是整个架构的精髓:
go
// relay只做一件事:
// 读取数据包头部标记 → 转发给对应transceiver
// 不解密、不解码、不持有状态每条relay持有的信息:
- 一条内存中的转发映射(客户端→transceiver)
- 几个监控计数器和过期定时器
没有:持久化、协议参与、任何解码操作
如果relay重启了,下一个数据包到达时就能自动重建路由。
解决首响应问题
Realtime API的核心挑战:用户第一个数据包到达relay时,relay还没有任何关于这个用户的信息。
解决方案:利用WebRTC自带的ICE ufrag机制
bash
# 通话建立流程:
1. transceiver分配会话状态
2. 在SDP应答里返回共享的relay虚拟IP和UDP端口
3. 客户端发第一个STUN binding request
4. relay只解析包头的ufrag字段,解码出路由提示
5. 立刻转发给对应transceiver后续所有包走同一条已建立的路径。
Global Relay全球部署
如果用户在北京,数据包要跑到美国西海岸才处理,单程延迟就超过150ms。
解决:让数据包尽早进入OpenAI自己的高速网络
Global Relay(全球分布)
↓
用户数据包在离自己最近的入口进入OpenAI网络
↓
通过内部骨干网到达transceiver
↓
比直接穿越公网:延迟更低、抖动更小、丢包更少为什么用Go写
做实时媒体转发,常规选择是C/C++或Rust,有些团队甚至上kernel bypass。
OpenAI用Go的理由:
| 优化 | 作用 |
|---|---|
| SO_REUSEPORT | 多个relay进程共享UDP端口,内核自动分配包 |
| runtime.LockOSThread | goroutine钉在固定线程,同一会话的包倾向落在同一CPU核心 |
| 预分配内存缓冲区 | 最小化数据拷贝,避免GC |
结论:当前负载用Go配合内核优化已经够用,先跑起来,不够再换kernel bypass。
开源依赖:使用了Pion(Go语言的WebRTC开源库)。
三条设计原则
| 原则 | 说明 |
|---|---|
| 硬性状态集中在一个地方 | transceiver拥有ICE、DTLS、SRTP和会话生命周期,relay只转发 |
| 在已有信息上做路由 | ICE ufrag是协议自带标识符,首包到达就能路由 |
| 够用就不换 | Go配合内核优化对当前负载够用,不盲目上kernel bypass |
技术栈总结
| 组件 | 技术 |
|---|---|
| 实时通信协议 | WebRTC |
| 核心架构 | relay + transceiver |
| 编程语言 | Go |
| WebRTC库 | Pion |
| 容器平台 | Kubernetes |
| 负载均衡 | Global Relay |
| 缓存 | Redis(容灾映射) |
核心认知
实时语音AI能跑起来,靠的是基础设施让延迟变得感知不到。 OpenAI改变的是WebRTC部署的内部形态,但没有改变客户端对WebRTC协议的预期。
📖 官方博客:openai.com/index/delivering-low-latency-voice-ai-at-scale/
