Go 与 Python 谁更适合 LLM 网关?KTMS 基准测试对比 LiteLLM 等九款网关
来源标题:Go vs. Python for an LLM Gateway: What We Measured
KTMS(Go 编写)与 LiteLLM Proxy(Python)等九款自托管 LLM 网关在同一云硬件上完成可复现基准测试,量化了附加延迟、首 token 时间、吞吐量、内存、CPU 和启动时间。测试方指出网关负责转发字节而非运行模型,本质上属网络问题,Go 单进程模型与 Python 多 worker 架构因而产生差异。测试还发现其自身代码中的一个 bug,并说明了数据结论的适用边界。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
在相同云硬件上,对 KTMS (Go) 与 LiteLLM Proxy (Python) 以及另外八个自托管网关进行的可复现基准测试:新增延迟、首 token 时间、吞吐量、内存、CPU 和启动时间。
Python 是机器学习的语言,因此大多数团队首先想到的是 Python 网关。我们用 Go 构建了 Keystone. The Model Switchboard. (KTMS),也就是我们的自托管 LLM 网关。网关并不运行模型。它在你的服务和模型供应商之间搬运字节,而这是一个网络问题。
“Go 更轻量”说起来容易,验证起来难,所以我们搭建了一个可复现的基准测试台,在相同硬件上让 KTMS 与 LiteLLM Proxy 对比,然后又与另外八个自托管网关对比。本文会逐一介绍我们测量了什么、测试台在我们自己代码里发现的一个 bug,以及这些数字在哪些地方不再适用。
LLM 网关位于每次模型调用的请求路径上:
像 LiteLLM 这样的 Python 网关运行在 FastAPI 和 Uvicorn 上。为了利用多核,它们会启动多个 worker 进程,因为存在全局解释器锁。每个 worker 都会加载自己的一份解释器、供应商 SDK 和数据模型。Go 服务则是单进程:运行时在所有核心上调度 goroutine,并共享一组连接池。
整个测试台在 ktms/bench 开源,一条命令即可重新运行。Terraform 会创建一个隔离网络和两台 VM,脚本运行整套测试,然后销毁所有资源。
runner VM 网关 VM(私有网络) ┌──────────┐ HTTP ┌────────────────────────────────────────┐ │ k6 │ ────────▶ │ KTMS ──────┐ │ │ │ │ LiteLLM ───┼──▶ mock OpenAI upstream │ └──────────┘ │ (direct) ──┘ │ └────────────────────────────────────────┘ 硬件:网关使用 Google Cloud c3-standard-4(4 vCPU、16 GB、x86_64),负载生成器使用另一台完全相同的 VM,这样 k6 永远不会和网关争抢 CPU。 版本:KTMS 由源码构建,包含第 4 节所述的修复;LiteLLM v1.106.0(发布在被入侵的 1.107.0 之前,我们从未运行过该版本),配置 4 个 Uvicorn worker(每核一个),仅使用 master key 且无数据库,这是它最精简的模式;k6 2.3.0。 上游:一个用 Go 编写的小型 mock,模拟 OpenAI Chat Completions API,响应固定,延迟可配置。真实供应商会引入数百毫秒的噪声,使各次运行无法比较。这个 mock 会统计收到的请求数,从而证明没有任何响应来自网关缓存。 基线:每个场景还会在没有网关的情况下运行(“direct”),因此“新增延迟”指的是网关延迟减去直连延迟。 一次只运行一个网关:每次运行前会停止其他网关。 内存:仅统计网关容器的 cgroup RSS。KTMS 还需要 Postgres 和 Redis,这些不计入。此模式下的 LiteLLM 两者都不需要。
我们运行了整套测试两次。对于每个网关,延迟、内存、CPU 和最大吞吐量在两次运行之间的差异都在约 10% 以内;LiteLLM 的 p99 是例外。图表显示的是两次运行的均值。
非流式请求,稳定在 200 RPS,上游即时响应。 KTMS 在中位数上增加约 0.6 ms,在 p99 上增加约 1.1 ms。LiteLLM 在中位数上增加约 8.5 ms,其 p99 在一次运行中为 27 ms,另一次为 229 ms,因此图表中的均值只能作为粗略参考。
50 个并发流,每个流包含 20 个块,块间间隔 15 ms。 两个网关都送达了每个流的每个块。KTMS 在上游发出首个 token 后约 0.7 ms 转发它。LiteLLM 则需要约 160 ms。它并没有合并块:整个流只是开始得更晚,并同样晚约 160 ms 结束。
提供的速率从 250 RPS 以每 20 秒一步攀升到 4,000 RPS,上游延迟为 10 ms。当至少 95% 的提供速率成功通过且错误率低于 1% 时,该步骤才算作持续通过。 KTMS 跟上了每一步,包括我们测试的最高值 4,000 RPS。其 p99 在 2,000 RPS 之前保持在 13 ms(直连:11 ms),在 4,000 RPS 时增长到 26–43 ms。LiteLLM 持续在 250 RPS。在 500 RPS 时,其三分之一的请求已经失败,从 1,000 RPS 起,它稳定在约 570 RPS 左右,同时一半的请求触发了 5 秒的客户端超时。
空闲时,KTMS 使用约 7 MiB。LiteLLM 在 4 个 worker 下使用约 2.5 GiB,每个 worker 约 0.6 GiB。在 200 RPS 下进行 5 分钟浸泡测试期间,KTMS 保持在约 20 MiB,LiteLLM 保持在约 2.9 GiB,两者都没有向上漂移。KTMS 在整个运行期间的峰值为 41–46 MiB,包括 4,000 RPS 的测试,远低于我们 Helm chart 中 512 MiB 的限制。
图表有意使用线性刻度。KTMS 的柱子很难看清,而这就是结果。
在浸泡测试期间,KTMS 每 1,000 个请求消耗约 0.63 s CPU 时间。LiteLLM 消耗约 9.7 s,是前者的十五倍。这个数字决定了给定流量水平需要多少核。
从 docker start 到通过网关回答第一个请求,KTMS 耗时约 170 ms,LiteLLM 耗时约 32 s。该时间包括启动容器本身,并且 KTMS 数据库已经完成迁移。
镜像大小也同样差异巨大。对于 linux/amd64,KTMS 精简镜像拉取约 21 MB,解压后约 67 MB。LiteLLM 镜像拉取约 420 MB,解压后约 1.3 GB。
第一次完整运行的结果与上面的图表并不一致。KTMS 在 2,000 RPS 之前表现正常,然后在 3,000 RPS 时 96% 的请求失败,每个失败都在一毫秒内发生。日志显示 connect: cannot assign requested address:容器用尽了出站端口。
原因是一行缺失的配置。OpenAI、Anthropic 和 Gemini SDK 回退到了 Go 的默认 HTTP transport,它每个主机只保留两个空闲连接。超过两个的每个并发请求都会向上游打开一个新连接,而关闭的连接堆积在 TIME_WAIT 中,直到没有端口可用。现在所有供应商客户端共享一个 transport,每个主机最多保留 1,024 个空闲连接。同样的运行随后在 4,000 RPS 下无错误地持续通过,并且 2,000 RPS 时的 p99 从 50–60 ms 降至 13 ms。
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。