qps/并发限制

接入与开发

面向调用大模型 API 的开发者,讲清 QPS 与并发限制分别卡的是什么、为什么会被限流,以及怎么测量、排队、降级和长期规避。

接口本地跑得好好的,一上批量任务就开始报限流,或者延迟突然拉长——多半不是代码写错了,是撞上了 QPS 或并发上限。这两个限制经常被混着叫「限流」,但卡的维度不一样,处理方式也不一样。这篇不重复协议兼容和客户端配置,只讲工程侧怎么量、怎么排队、怎么降级、怎么留证据。

接口在本地跑得好好的,一上批量任务就开始报限流,或者延迟突然拉长——这种时候多半不是代码写错了,是撞上了 QPS 或并发上限。这两个限制经常被混着叫「限流」,但它们卡的维度不一样,处理方式也不一样。频率超了和同时开工数超了,表现、排查路径、修法都是两套东西。

这篇只讲工程侧怎么量、怎么排队、怎么降级、怎么留证据,不重复协议兼容和客户端配置那些已经有人讲透的部分。先说清一个前提:QPS 和并发这两个词在不同平台的口径可能有细微差别,真正卡你的那条线写在对方的限制说明和报错文本里。拿不准的时候,到站内对应的平台页面核对当时的数字与限制口径,别照搬别人的经验值。

QPS 和并发限制卡的其实不是一回事

QPS 是单位时间窗口内的请求次数上限,卡的是「频率」;并发是同一时刻在途请求数上限,卡的是「同时开工数」。频率超了通常报的是短窗口限流,等一下再发可能就过了;并发超了常见表现是排队、变慢,严重时直接拒绝。同一份代码在单机顺序调用下往往没事,一开线程池或异步并发就暴露,因为瞬时在途数被放大了好几倍。

QPS 超限
短窗口内请求次数太多,通常等一小段时间后单次重试就能成功,属于节奏问题。
并发超限
同时在途的请求数太多,表现为排队、耗时拉长或直接拒绝,属于槽位问题。
在途请求
已经发出但还没拿到完整响应的请求,流式响应和长上下文会长时间占用它。
瞬时峰值
一秒内突发一批请求,平均 QPS 不高但真实峰值很高,是最常见的踩线方式。
判断维度更像 QPS 超限更像并发超限
报错文本强调频率、速率、窗口强调同时、并发、槽位
重试后表现隔一会儿单次重试成功不降并发就一直失败
延迟特征延迟基本正常,直接失败明显排队变慢,然后失败
触发时段集中在批量任务启动瞬间贯穿整个长任务期间
修法方向加令牌桶、控制发送节奏加信号量、限制工作池大小

判断当前撞的是哪一种,按这几条走:先看报错文本里的关键词,再做单次重试测试,再观察是持续失败还是只在峰值时段失败。三条对不上就说明可能不是限流,而是别的环节出了问题,别急着加退避。

为什么会被限流:从你的代码到上游的整条链

限流不是只来自一处。从你的代码发出去,到最终模型返回,中间至少经过四层:你自己的调度逻辑、中转站或网关、官方账号级别的额度、以及底层推理资源的真实排队。任何一层卡住,你在客户端看到的都像是「被限流」,但修法完全不同。中转站常按渠道分摊额度,所以同一个 Key 在不同站点体感可能差很多。

  • 你自己的调度逻辑:无上限线程池、Promise.all 打一批、失败立刻原地重试,都会把瞬时并发推高。
  • 中转站或网关:可能按渠道、按用户、按时间段做额外限制,不一定和官方口径一致。
  • 官方账号级别:按账号或 Key 维度算,买到的配额档不同,可用的并发上限就不同。
  • 底层资源排队:即便额度没超,模型侧真实算力紧张时也会让请求排队,表现为慢而不是直接报错。

有个容易被忽略的点:重试本身也是请求。没有退避的立即重试,等于在限流发生时主动再加一批流量,越重试越被限。另一个是流式请求和长上下文请求,它们占用连接的时间更久,即便 QPS 不高,也可能把并发槽位占满,把短请求挤出去。

先量基线:你到底能跑多快、慢在哪

调参之前先量。没有基线数据,后面所有优化都是猜。第一步是分清「慢」和「被限」:到 /tools/speed-test 测出延迟基线,把首包时间和整体耗时分开看。首包慢通常是排队或冷启动,整体耗时长可能是上下文太长或输出太长,两者处理方式不一样。

  1. 定梯度从并发 1 开始,按固定梯度往上加,比如每档翻倍或加固定步长,每档跑足够多的样本再进下一档。
  2. 记三个数每档记录成功率、平均耗时和报错类型。第一个成功率明显下滑的档位,就是你实际的可承受并发布点。
  3. 做对照同一档位下分别测短请求和长请求,看槽位是被谁占掉的。长请求单独占池往往能让短请求成功率立刻回升。
  4. 核口径到 /official-api 与 /ai-nav/category/ai-model-platform 看该平台的计费与限制口径,确认是不是买到了低配额档,而不是代码写错。

把每次请求的耗时、状态码、重试次数都记下来。最低要求是能按分钟画出成功率曲线,这样限流会先出现在曲线拐点上,而不是等你翻报错日志才发现。基线数据最好固定跑法、固定参数,否则今天和下周的数字没法比。

排队与节奏控制:把并发压到限制之下

控制并发的核心做法是给「同时开工数」加一个硬上限。固定大小的信号量或工作池是最直接的手段,别用无上限的线程池,也别直接用 Promise.all 把一批任务全打出去。需要控制 QPS 时用令牌桶,速率要留出余量,贴着上限跑迟早会撞峰值。

控制目标推荐手段关键参数怎么定
限制同时在途数信号量 / 固定工作池取基线测试里成功率仍稳定的档位,再往下留一点余量
限制单位时间请求数令牌桶按业务峰值估算,别卡在限制线上,留出突发余量
批量任务分批 + 批间间隔批内串行或小并发,失败任务进单独重投队列
长耗时请求单独设池和短请求隔离,避免它把槽位全占住
重试流量独立限流重试走单独的队列和速率,不和正常请求抢额度
  • 批量任务先分批再并发,批内可以串行或小并发,批间留固定间隔。
  • 失败的任务不要原地重试,丢进单独的重投队列,由统一的重试器带着退避去处理。
  • 长耗时请求单独设池,别和短请求共用一个信号量。
  • 重试流量单独限流,否则正常请求和重试请求会互相挤。
  • 把并发数、令牌桶速率做成配置项,别硬编码,不同环境可以调。

判断是否收敛很简单:稳定运行一段时间后,限流次数应该接近零,同时成功率维持在你能接受的水平。如果并发压下去了但成功率没回来,说明瓶颈不在你这层,往上排查渠道或账号额度。

退避、降级与失败处理

重试要带指数退避加随机抖动,并设最大次数与总时间上限。没有上限的重试在限流场景下是灾难,会把额度烧光,还会把问题掩盖成「系统很慢」。退避的基数、上限和抖动范围都写成配置,不同渠道可以给不同参数。

  • 可重试:限流、超时、5xx,值得退避后重试,但要有次数和总时间上限。
  • 不可重试:参数错误、鉴权失败、内容不合规,重试多少次结果都一样,直接落库报错。
  • 降级顺序:先换同能力的备用渠道,再降级到更小的模型或更短的上下文,最后才把任务标记为失败。
  • 降级要有开关和阈值,命中降级时记一条日志,否则事后根本不知道有请求走了降级路径。
  • 重试和降级的判断条件、阈值全部配置化,别写死在业务逻辑里。

有一点要提前想清楚:降级换来的是可用性,代价是结果质量可能不一致。如果业务对输出质量有硬要求,降级路径就必须经过额外校验,或者干脆不降级、直接排队等低峰。这个取舍按业务定,没有通用答案。

把限流变成可观测指标

限流本身应该是一个可观测的指标,而不是只能从报错里被动发现。至少记录这些字段:请求标识、时间戳、目标渠道、状态码、耗时、重试次数、是否命中降级。有了这些,一次请求的慢和失败都能复盘,而不是只能看到「有时候会挂」。

指标按什么粒度用来判断什么
成功率分钟级聚合整体健康度,拐点常先于报错出现
P95 耗时分钟级聚合是否在排队,以及排队从什么时候开始
限流次数分钟级聚合容量是否够,是否有人绕过限流器
重试次数按渠道聚合重试是否在放大压力
降级命中数按任务类型聚合哪些业务在靠降级维持

给限流次数设告警阈值,连续触发就说明容量不够,或者有人绕过了限流器直接打接口。日志字段的设计可以参考 /tools/quality-monitor 的思路,重点是让指标能对上一次具体的失败,而不是只留一个总数。告警别只报数量,带上渠道和任务类型,否则收到告警也不知道从哪查。

长期规避:容量、渠道与任务重排

短期靠排队和退避能活,长期得回到容量规划。先算清业务峰值需要多少并发和多少 QPS,再去对额度,别等线上报错才补。非实时任务挪到低峰时段跑,是最省事也最容易被忽略的一招:同一批任务换个时间点,可能就完全不撞线。

  • 先算峰值需求,再对账号或渠道的额度,缺口提前谈。
  • 多渠道分流能摊薄单点压力,但要接受不同渠道能力和限制不一致,按渠道分别设速率。
  • 非实时任务重排到低峰时段,优先级最低的批量任务放最后。
  • 账号级硬上限无论怎么调代码都绕不过去,这时候要谈额度或换接入方式,而不是继续优化重试。
  • 定期回看多渠道的实际可用性和限制变化,可以结合 /rankings 与 /sites 上汇总的信息做对照。

如果你在选接入方式,建议把「限制口径是否透明、超额时怎么报错」当成一个硬指标来评估,而不只是看能不能调通。相关信息可以到 /ai-nav/category/ai-model-platform 对照,价格与额度的当期数字以站内页面为准。

这些做法什么时候不适用

上面这套不是万能的,有几种情况硬套反而更糟。第一,如果上游本身不稳定而不是限流,退避和排队只会把失败拖得更久;这种情况下应该先做连通性与可用性验证,确认是限流还是服务本身在抖。第二,强实时的交互场景不适合排队等待,用户等不起,此时更该做前置降级或给明确反馈,而不是默默重试。

  • 单次调用、低频率的调试场景不用上这套复杂度,加一层限流器反而增加排查成本。
  • 上游不稳定时先验证可用性,别把抖动当成限流去调参。
  • 强实时场景不要默默排队,给用户明确反馈或走快速降级。
  • 配额差距过大时,优化重试没有意义,该谈额度或换接入方式。
  • 不同渠道限制口径不一致时,别用一套参数通吃,按渠道分开配。
限流不是敌人,是上游在告诉你当前节奏超过了它的承受能力;你要做的是量出那条线,然后稳定地待在它下面。
下一步把你现在的并发数、重试策略和日志字段过一遍,再拿基线数据去对平台口径。先到 /tools/speed-test 量一次当前的真实延迟和成功率,再到 /official-api 与 /ai-nav/category/ai-model-platform 核对限制口径,两边的数字对上了,才知道该压并发还是该换渠道。

常见问题

QPS 限制和并发限制,先解决哪个?

先看报错和日志:如果是短时间高频被拒,优先控 QPS;如果是批量同时发起后整体变慢或被拒,优先降并发。多数情况两者要一起设,先压并发到稳定,再逐步调频率。

加了重试为什么反而更容易被限流?

因为立即重试等于在限流窗口内又追加了请求,瞬时并发和频率都被推高。重试必须配退避和随机抖动,并设总次数上限,否则失败请求会形成放大效应。

并发限制设成多少合适?

没有通用数字。做法是从并发 1 开始逐档往上加,记录成功率和耗时,取成功率仍在可接受范围、且留有一定余量的那一档。上游额度、模型大小、请求长度都会影响这个值。

流式输出会不会更容易触发限制?

会更早占满并发槽位,因为一条流式连接保持时间更长,同样的并发数下同时在途的请求更多。所以流式场景的并发上限通常要比非流式设得更保守。

被限流之后流量怎么恢复?

先停止新增请求,让退避队列自然消化;确认成功率回升后再逐步放量,不要一次恢复到原速率。同时核对是配置问题还是额度问题,必要时改用备用渠道。

本文依据各平台公开文档与官方公告整理,只做中文归纳;具体做法、参数与限制以官方文档为准。