LiteLLM Rust 网关 Realtime 基准测试:连接池开关对 WebSocket 网关开销的实测与源码剖析
本文基于 LiteLLM Rust 网关自带的 realtime 基准测试文档,解读网关代理 OpenAI Realtime WebSocket 时究竟增加了多少延迟、预热连接池(warm pool)在其中消除掉了哪个环节,并给出完整的压测参数、结果数据和复现方法;读完你可以理解"网关开销全部集中在会话建立阶段"这一结论的测量依据,并掌握 REALTIME_POOL_SIZE 等配置项的取值原理与源码级实现。
基准测试目标:量化网关与连接池各自的贡献
LiteLLM 的 Rust 网关(litellm-rust/crates/ai-gateway)提供 GET /v1/realtime 路由,将客户端 WebSocket 直接拼接(splice)到 OpenAI 的 realtime 上游。围绕这个路由的基准测试回答两个问题:
- 相比客户端直连 OpenAI realtime WebSocket,经过网关多出多少延迟;
- 预热的上游连接池开启后,又消除了多少这部分开销。
压测条件为:5000 次调用 / 500 并发,网关侧 10 个实例,连接池开启(REALTIME_POOL_SIZE=64),上游模型为 OpenAI gpt-realtime。每个测试腿(leg)各运行两次,时间单位均为 ms。
指标定义:按连接的四个阶段拆分开销
每条连接被拆成四个阶段来测量,这套拆分是理解结果表的关键:
- dial:TCP + TLS + WebSocket 升级握手;
- session:从 WS 升级完成到收到
session.created事件——这正是连接池要消除的阶段; - 1st-audio:从发送
response.create到收到第一个音频 delta(这段是 OpenAI 自身的推理时间); - total:完整墙钟时间。
实测结果:6 项指标中 4 项快于直连
| 指标 | 直连 OpenAI | 网关(池 ON) | 开销 (ms) | 相对 OpenAI |
|---|---|---|---|---|
| 成功率 (%) | 99.8 | 99.8 | — | — |
| dial p50 (ms) | 276 | 158 | −118 | 更快 |
| session p50 (ms) | 7 | 0 | −7 | 更快 |
| 1st-audio p50 (ms) | 440 | 664 | +224 | 更慢¹ |
| total p50 (ms) | 816 | 1010 | +194 | 更慢¹ |
| total p95 (ms) | 2152 | 1970 | −182 | 更快 |
| total p99 (ms) | 2692 | 2610 | −82 | 更快 |
结果表完整继承自 realtime 基准 README,其核心结论有三点:
- 网关在 6 项指标中的 4 项上快于直连。预热连接池使 session 阶段在中位数上降到亚毫秒级——约 76% 的连接命中了连接池,约 70% 的连接 session 阶段耗时 < 1 ms。
- 两行"更慢"不是网关开销:
1st-audio是 OpenAI 自己的推理时间(网关只负责转发),网关腿恰好赶上推理变慢,并拖着total p50一起变慢(表中脚注 ¹ 的含义)。 - 对照实验(Pool OFF,
REALTIME_POOL_SIZE=0)下 session p50 高达 367 ms——这就是连接池所消除的"每次连接都要重新拨号"的开销。
对照 realtime 路由设计文档可知,网关的 realtime 开销全部集中在会话建立阶段:每次客户端接入都拨一个全新的上游 WS 并等待 session.created,实测该阶段约 360 ms,而直连仅约 7 ms;dial、首包音频与流式转发本身几乎零增量。因此唯一有效的优化杠杆,就是把每次连接的握手从关键路径上挪走——预热连接池正是干这件事的。
源码剖析:预热连接池如何实现"亚毫秒 session"
池的键与"一次性"语义
池实现位于 realtime_pool.rs。每个上游连接由 UpstreamKey 三元组唯一确定:
pub struct UpstreamKey {
pub model: String,
pub api_key: String, // 含 api_key → 无跨租户复用
pub api_base: Option<String>,
}
api_key 被纳入池键意味着一条热连接只会被交给解析到同一个 key 的请求,杜绝跨租户复用;Debug 实现中 api_key 恒显示为 [REDACTED],避免密钥泄露进日志。由于 realtime 会话不可复用(一条热连接只服务一个会话),池的大小应匹配单实例的峰值并发连接速率,而不是当前存活连接数。
热连接为何与冷连接不可区分
OpenAI 在 WebSocket 建连后会主动下发一条 session.created,无需客户端先发任何事件。池的 warm_one()(realtime_pool.rs#L437-L452)拨号后立即用 read_event() 恰好预读并缓冲这一帧,在此之前不读任何其他数据、也不向 socket 发送任何内容。因此客户端拿到热连接后发出的第一个 session.update 行为与全新会话完全一致。
取用热连接的 take()(realtime_pool.rs#L259-L285)做一次本地 Vec::pop() 并通过非阻塞存活检查(is_dead())——热连接在 session.created 之后应保持静默,任何挂起的 Close/Err/意外数据帧都视为不健康并丢弃;永不阻塞:池空或全死时返回 None,调用方回退到原生的全新拨号路径。这就是"池只能让连接更快、不会更慢或更脆弱"这一设计承诺的代码体现。
请求路径:先取池,miss 即冷拨号
路由服务层 service.rs 的逻辑非常直接:
// Warm path: take a pooled upstream (handshake already paid) and relay its
// buffered session.created immediately. On miss/dead socket fall through.
if let Some(key) = upstream_key(provider_model, params.api_key.as_deref(), params.api_base.as_deref())
&& let Some(handoff) = pool.take(&key)
{
return crate::io::realtime::realtime_warm(...).await;
}
// Cold path: fresh dial (the original behavior).
crate::io::realtime::realtime(...).await
热路径与冷路径最终汇入同一个 splice() 拼接循环(realtime.rs#L132-L208):热路径只是把缓冲的 session.created 作为 prelude 先转发给客户端,随后双向转发逻辑与冷路径完全相同。splice 内置空闲超时(默认 300 秒,任何一帧活动都会重置它),超时即回收卡死会话的 task 与上游 TCP socket。
后台补水:并发拨号与失败退避
后台补水池任务每 250 ms 唤醒一次(REPLENISH_TICK,见 realtime_pool.rs#L50-L62),对每个已注册的 key 执行 replenish_all():
- 并发拨号:缺多少条热连接就同时发起多少个拨号(
join_all)。源码注释解释了为什么必须并发——串行补全会让"全量回填"耗时约needed × 350 ms,在高连接速率下池会"抽干比回填快",导致大多数连接 miss;并发拨号让被抽干的池在大约一个握手窗口内重新填满,把亚毫秒热交接从"幸运命中的长尾"变成"中位数"。 - 指数退避:某个 key 的预热拨号全部失败(凭据无效、上游不可达)时,该 key 进入 500 ms 起步、上限 30 s 的指数退避(
BACKOFF_BASE→BACKOFF_MAX),而不是每个 tick 都重拨——这防止坏 key 耗尽上游速率限制、拖累正常冷路径流量;任一次拨号成功立即清零退避。 - 过期回收:热连接超过
max_idle(默认 30 s)即被reap_stale()关闭并替换,用于约束空闲计费并规避 OpenAI 的空闲断连。 - 池本身以
Arc<RealtimePool>形式挂在网关共享状态上(state.rs),REALTIME_POOL_SIZE=0时使用RealtimePool::disabled(),每次take必然 miss,行为退化为纯冷拨号——这也是基准测试中 Pool OFF 对照腿的实现方式。
配置参数与容量规划
两个环境变量在 realtime_pool.rs#L119-L154 的 PoolConfig::from_env() 中解析,非法值只告警并回退默认值、不会导致启动失败:
| 环境变量 | 默认值 | 含义 |
|---|---|---|
REALTIME_POOL_SIZE |
4 |
每个 key 的目标热连接数;0 禁用连接池(纯冷拨号) |
REALTIME_POOL_MAX_IDLE_SECS |
30 |
热连接最长空闲时间,超过即关闭并替换 |
容量规划公式(继承自 realtime 路由 README):
REALTIME_POOL_SIZE ≈ peak_concurrency / instance_count
例如 500 并发、10 实例 → 每实例约 50–64,即本次基准采用的 64。由于补水是并发的,过量配置只是多占上游空闲 socket(有计费与超时风险),所以热连接刻意保持短生命周期(REALTIME_POOL_MAX_IDLE_SECS)。
复现方法
压测负载生成器是一个独立的 Go 仓库(litellm-realtime-bench,地址见 基准 README)。构建并运行两条腿:
# 构建负载生成器(仓库地址见上述 README,勿提交密钥——一律经 -key 传入)
go build -o wsbench .
# 腿 1:直连 OpenAI(基线)
./wsbench -host api.openai.com -key "$OPENAI_API_KEY" -m gpt-realtime -n 5000 -c 500 -t 60
# 腿 2:经网关——分别以 pool ON 与 REALTIME_POOL_SIZE=0 各跑一次
./wsbench -host <gateway-host> -key "$LITELLM_MASTER_KEY" -m gpt-realtime -n 5000 -c 500 -t 60
网关侧需以环境变量方式启动:OPENAI_REALTIME_MODEL=gpt-realtime、OPENAI_API_KEY、LITELLM_MASTER_KEY、REALTIME_POOL_SIZE、HOST=0.0.0.0。在 N 实例上跑 500 并发时,按 ≈ 500 / N 给每实例配置池大小(本基准 10 实例取 64)。负载生成器自身的 README 覆盖了如何在多 vCPU runner 上跑 500 并发腿。
小结
这份基准把"网关代理 realtime WebSocket 值不值"拆成了可量化的答案:网关对 dial 和 session 阶段是净收益(池 ON 时 session p50 从 367 ms 降到 0 ms),对 p95/p99 长尾同样是净收益;唯一"变慢"的 1st-audio 属于 OpenAI 推理时延,与网关无关。从源码结构看,这套结论由三处设计共同支撑:session.created 单帧预缓冲保证热/冷会话语义等价、take() 永不阻塞保证池只优化延迟不引入正确性依赖、并发补水加指数退避保证供给速率跟上峰值连接速率且坏 key 不拖垮整体。对读者而言,可直接复用的决策点是:按 峰值并发 / 实例数 配置 REALTIME_POOL_SIZE,保持 REALTIME_POOL_MAX_IDLE_SECS 默认 30 s 以控制空闲成本,并在压测时用 REALTIME_POOL_SIZE=0 跑一腿对照来验证池的实际收益。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00