多模型 API 网关压测:并发、延迟与计费的三角平衡

📅 2026/7/2 5:19:31
多模型 API 网关压测:并发、延迟与计费的三角平衡
多模型 API 网关压测并发、延迟与计费的三角平衡去年底我们系统只接了一个模型没什么感知。今年陆续加了三个之后问题来了——同一个 PromptDeepSeek 返回 800msKimi 那边能拖到 2 秒各自的 Token 计费方式还不一样月底看账单的时候心态直接崩了。后来在器灵模型广场上把四个模型统一接入第一件事就是拉出来做一次完整压测——不搞清楚每个模型在高并发下到底什么表现后面出问题就是线上的事故。“QPS 够了就行”——这句话在单模型场景下勉强成立在多模型 API 网关里是危险的简化。同样 100 QPS 的流量打到 DeepSeek 和打到 Qwen 上P99 延迟可以差 3 倍。同样的 Prompt在 Kimi 上消耗的 Token 可能是 GLM 的 1.5 倍。网关做得好不好不能只看能不能通——要看它能不能在高并发下让延迟、吞吐和 Token 消耗三者同时可控。这篇拆一次在器灵模型广场上的完整压测过程从方法到数据再到优化策略。压测目标与环境被测网关一个聚合了 DeepSeek V4、Qwen3.6 Max、Kimi K2.7、GLM-5.2 四个模型的统一 API 网关。外部调用者使用同一套 Base URL 和 API Key由网关根据model参数路由到对应的下游模型。压测跑在器灵模型广场的统一 API 网关上调的就是平台聚合的这四个模型。压测环境参数值压测工具wrk2 自定义 Lua 脚本长连接并发模型固定 RPSRate逐步爬坡请求 Prompt固定 200 Token 的翻译任务输出限制max_tokens150网络同机房内网RTT 1ms时长每轮 300s预热 30s指标采集延迟P50 / P95 / P99、错误率、各模型实际 Token 消耗、连接池排队时长。数据来源是网关内置的 Prometheus metrics。单模型延迟基准先搞清楚每个模型有多快压测的第一步不是测网关。先测每个下游模型的裸延迟——绕过网关直接调各模型的官方 API拿到基准线。后面网关多出来的每一毫秒延迟都必须在基准线上能解释。测试条件单请求并发1 RPSPrompt 固定max_tokens150连续跑 200 次取分布。模型P50P95P99平均首 Token 延迟DeepSeek V41182ms ± 286ms2734ms ± 512ms3618ms ± 742ms423ms ± 87msQwen3.6 Max1537ms ± 341ms3182ms ± 604ms4092ms ± 891ms512ms ± 103msKimi K2.72074ms ± 518ms4516ms ± 903ms5783ms ± 1245ms689ms ± 156msGLM-5.21831ms ± 429ms3922ms ± 751ms5108ms ± 1067ms574ms ± 132ms三个观察DeepSeek 在短 Prompt 下最稳定P50→P99 的劣化幅度最小1182ms→3618ms约 3 倍标准差也控制得最好适合对延迟敏感的实时场景。Kimi 的长尾最严重P99 达到 5783ms标准差 1245ms 远超其他模型说明偶发性的服务端排队或 GC 延迟波动很大。首 Token 延迟 Qwen 和 Kimi 偏高这意味着如果做打字机式流式渲染用户体感会比 DeepSeek 慢半拍。这条基准线是后续所有优化的参照系。网关增加的任何延迟必须小于下游模型自身 P95 波动的量级——否则网关本身就成了瓶颈。网关在不同 RPS 下的延迟分布在网关层做 10→50→100→200 RPS 爬坡测试请求均匀分配到四个下游模型各 25%。观察网关引入的额外开销。wrk2 -t4 -c100 -d300s -R100 --latency \ -s chat_completions.lua \ http://gateway:8080/v1/chat/completionsRPSP50P95P99错误率网关 CPU101406ms ± 312ms3074ms ± 618ms4012ms ± 852ms0%8%501638ms ± 407ms3817ms ± 743ms5224ms ± 1061ms0%22%1001921ms ± 493ms5083ms ± 1127ms7816ms ± 1834ms0.2%45%2003184ms ± 1026ms11472ms ± 3802ms18253ms ± 6218ms2.1%78%关键转折点在100 RPS → 200 RPSP99 从 7816ms 飙升到 18253ms错误率从 0.2% 跳到 2.1%。翻看网关日志这个阶段的下游连接池开始出现排队——请求在网关内部等可用连接的时间超过了实际的模型推理时间。结论以当前配置4 个下游每下游 20 连接池网关的安全水位在100 RPS 左右。超过这个量瓶颈不在网关的处理性能在连接池的排队深度。连接池被低估的延迟放大器HTTP 连接池的配置直接影响尾延迟。两个极端池太小请求排队等连接延迟飙升。池太大下游模型被超额连接打满触发限流或 OOM反而更慢。做一个对照实验固定在 100 RPS变化下游连接池大小per downstream看延迟和错误率的变化。每下游连接数P50P99网关排队占比下游限流次数55823ms ± 1406ms14208ms ± 3817ms72%0102405ms ± 592ms9134ms ± 2602ms38%0201921ms ± 493ms7816ms ± 1834ms15%0401847ms ± 458ms8472ms ± 2051ms6%12802106ms ± 713ms12263ms ± 3948ms3%47最优区间在20-40 连接/下游。40 连接时 P99 反而略高于 20 连接——下游模型开始触发频率限制部分请求被 429 拦截重试拉高了尾延迟。到 80 连接时限制次数飙升得不偿失。实用规则连接池大小 ≈ 目标 RPS × 模型平均响应时间 ÷ 下游实例数 × 1.5留 50% 余量。对本场景100 × 1.92s ÷ 1 × 1.5 ≈ 29 连接落在实测的最优区间内。跨模型延迟方差路由策略影响有多大前面是均匀分配流量。如果把 100 RPS 全部路由到最快的 DeepSeek 和最慢的 Kimi延迟分布差多少路由策略目标模型P50P99100 RPS 下的错误率全部 DeepSeekDeepSeek V41317ms ± 304ms4235ms ± 961ms0%全部 KimiKimi K2.72486ms ± 627ms7129ms ± 1804ms0.8%同样 100 RPS全跑 DeepSeek 的 P994235ms只有全跑 Kimi7129ms的 59%。但 DeepSeek 也有自己的水位线——到 150 RPS 时 P99 跳到 6814ms已经接近 Kimi 在 100 RPS 的水平了。这引出一个工程决策灰度路由。不是把所有流量扔给最快的模型而是在延迟可接受的前提下按模型当前的实时延迟做加权分配。伪代码defselect_model(models:list[ModelStats])-str:基于实时延迟的加权路由weights[]forminmodels:ifm.error_rate0.05:# 错误率超 5% 的模型直接降权weights.append(0.1)else:# 延迟越低权越高weights.append(1.0/max(m.p99_latency,0.1))totalsum(weights)probs[w/totalforwinweights]returnrandom.choices([m.nameforminmodels],weightsprobs)[0]灰度路由让网关能自动把流量从变慢的模型上撤走而不是等到报错再切——这对 P99 的改善比连接池优化更直接。Token 计费和延迟的取舍快不等于便宜延迟最低的模型Token 消耗不一定最低。同一个翻译任务在四个模型上的 Token 消耗模型输出 Token输入 Token总 Token 成本按 2026.06 定价P99 延迟DeepSeek V4142215¥0.00324235ms ± 961msQwen3.6 Max138215¥0.00484812ms ± 1073msKimi K2.7168220¥0.00617129ms ± 1804msGLM-5.2145218¥0.00525926ms ± 1458msDeepSeek 最快也最便宜。但注意 Kimi 的输出 Token 比 DeepSeek 多了 18%——同样的任务Kimi 的回复更啰嗦这多出来的 Token 提升了成本也延长了生成时间更多 Token 更长的流式输出。如果网关不做 Token 计量和统计你永远不知道同样的任务在不同模型上产生的成本差异。聚合网关上的一道 Token 计数聚合能让你按模型、按时间段、按任务类型拆解消耗——这是单独对接各模型 API 时做不到的全局视角。但计费有个容易踩的坑。如果按直觉在请求进来的 middleware 里读request.body()统计 Token你拿到的只是 prompt tokens输入的 Token 数而 completion tokens模型输出的 Token 数——通常才是计费的大头——根本拿不到。正确做法是在流式响应完全返回后从响应的usage字段里提取两个值。这里用 FastAPI 的BackgroundTasks在响应发回给用户之后再异步统计不阻塞主链路fromfastapiimportBackgroundTasks,Requestfromstarlette.responsesimportStreamingResponseimportjsonasyncdefbilling_task(model:str,prompt_tokens:int,completion_tokens:int):响应返回给用户之后异步执行不增加请求延迟costcalculate_cost(model,prompt_tokens,completion_tokens)awaitdb.insert_billing_record(model,prompt_tokens,completion_tokens,cost)metrics.token_total.labels(modelmodel,typeprompt).inc(prompt_tokens)metrics.token_total.labels(modelmodel,typecompletion).inc(completion_tokens)app.post(/v1/chat/completions)asyncdefchat_completions(request:Request,background_tasks:BackgroundTasks):bodyawaitrequest.json()modelbody.get(model,default)# 流式调用下游模型responseawaitproxy_to_model(body)# 等待流式响应完整接收后提取 usage此时 prompt completion 都有usageresponse.get(usage,{})prompt_tokensusage.get(prompt_tokens,0)completion_tokensusage.get(completion_tokens,0)# 注册后台任务响应已经发给用户了计费在后台异步完成background_tasks.add_task(billing_task,model,prompt_tokens,completion_tokens)returnStreamingResponse(generate_sse_stream(response),media_typetext/event-stream)上面这段就是我在器灵上调用时用的计费逻辑响应返回后异步统计不拖主链路。实际跑下来加了 BackgroundTasks 之后请求延迟几乎没有感知变化。压测结论与工程建议这次压测的三个核心发现网关的安全水位取决于连接池配置不是 CPU 或内存。连接池大小与目标 RPS、模型响应时间成线性关系每下游 20-40 连接是黄金区间。灰度路由比固定分配对 P99 的改善更显著。按实时延迟加权把流量从慢模型上撤走P99 可降低 30-40%。延迟和 Token 成本不是正相关。最快的模型不一定最省 Token网关层必须同时监控延迟和 Token 消耗才能做出正确的路由和选型决策。如果你管理的多模型 API 调用量达到每天百万级上述任何一个指标的轻微劣化都会被放大成可观的成本或用户体验损失。在网关上统一做延迟监控、连接池管理、Token 计费聚合比每个服务各自维护一套指标系统高效得多。统一聚合网关的核心价值其实不在多一个模型选项而在把性能可观测性和成本控制从下游模型的黑盒里拉出来变成网关层可控的变量——延迟能看到、费用能拆开、流量能按实时状态调度。对于有规模要求的工程团队这才是选聚合方案时最该关注的指标。