接口本地跑得好好的,一上批量任务就开始报限流,或者延迟突然拉长——多半不是代码写错了,是撞上了 QPS 或并发上限。这两个限制经常被混着叫「限流」,但卡的维度不一样,处理方式也不一样。这篇不重复协议兼容和客户端配置,只讲工程侧怎么量、怎么排队、怎么降级、怎么留证据。
接口在本地跑得好好的,一上批量任务就开始报限流,或者延迟突然拉长——这种时候多半不是代码写错了,是撞上了 QPS 或并发上限。这两个限制经常被混着叫「限流」,但它们卡的维度不一样,处理方式也不一样。频率超了和同时开工数超了,表现、排查路径、修法都是两套东西。
这篇只讲工程侧怎么量、怎么排队、怎么降级、怎么留证据,不重复协议兼容和客户端配置那些已经有人讲透的部分。先说清一个前提:QPS 和并发这两个词在不同平台的口径可能有细微差别,真正卡你的那条线写在对方的限制说明和报错文本里。拿不准的时候,到站内对应的平台页面核对当时的数字与限制口径,别照搬别人的经验值。
QPS 和并发限制卡的其实不是一回事
QPS 是单位时间窗口内的请求次数上限,卡的是「频率」;并发是同一时刻在途请求数上限,卡的是「同时开工数」。频率超了通常报的是短窗口限流,等一下再发可能就过了;并发超了常见表现是排队、变慢,严重时直接拒绝。同一份代码在单机顺序调用下往往没事,一开线程池或异步并发就暴露,因为瞬时在途数被放大了好几倍。
- QPS 超限
- 短窗口内请求次数太多,通常等一小段时间后单次重试就能成功,属于节奏问题。
- 并发超限
- 同时在途的请求数太多,表现为排队、耗时拉长或直接拒绝,属于槽位问题。
- 在途请求
- 已经发出但还没拿到完整响应的请求,流式响应和长上下文会长时间占用它。
- 瞬时峰值
- 一秒内突发一批请求,平均 QPS 不高但真实峰值很高,是最常见的踩线方式。
| 判断维度 | 更像 QPS 超限 | 更像并发超限 |
|---|---|---|
| 报错文本 | 强调频率、速率、窗口 | 强调同时、并发、槽位 |
| 重试后表现 | 隔一会儿单次重试成功 | 不降并发就一直失败 |
| 延迟特征 | 延迟基本正常,直接失败 | 明显排队变慢,然后失败 |
| 触发时段 | 集中在批量任务启动瞬间 | 贯穿整个长任务期间 |
| 修法方向 | 加令牌桶、控制发送节奏 | 加信号量、限制工作池大小 |
判断当前撞的是哪一种,按这几条走:先看报错文本里的关键词,再做单次重试测试,再观察是持续失败还是只在峰值时段失败。三条对不上就说明可能不是限流,而是别的环节出了问题,别急着加退避。
为什么会被限流:从你的代码到上游的整条链
限流不是只来自一处。从你的代码发出去,到最终模型返回,中间至少经过四层:你自己的调度逻辑、中转站或网关、官方账号级别的额度、以及底层推理资源的真实排队。任何一层卡住,你在客户端看到的都像是「被限流」,但修法完全不同。中转站常按渠道分摊额度,所以同一个 Key 在不同站点体感可能差很多。
- 你自己的调度逻辑:无上限线程池、Promise.all 打一批、失败立刻原地重试,都会把瞬时并发推高。
- 中转站或网关:可能按渠道、按用户、按时间段做额外限制,不一定和官方口径一致。
- 官方账号级别:按账号或 Key 维度算,买到的配额档不同,可用的并发上限就不同。
- 底层资源排队:即便额度没超,模型侧真实算力紧张时也会让请求排队,表现为慢而不是直接报错。
有个容易被忽略的点:重试本身也是请求。没有退避的立即重试,等于在限流发生时主动再加一批流量,越重试越被限。另一个是流式请求和长上下文请求,它们占用连接的时间更久,即便 QPS 不高,也可能把并发槽位占满,把短请求挤出去。
先量基线:你到底能跑多快、慢在哪
调参之前先量。没有基线数据,后面所有优化都是猜。第一步是分清「慢」和「被限」:到 /tools/speed-test 测出延迟基线,把首包时间和整体耗时分开看。首包慢通常是排队或冷启动,整体耗时长可能是上下文太长或输出太长,两者处理方式不一样。
- 定梯度从并发 1 开始,按固定梯度往上加,比如每档翻倍或加固定步长,每档跑足够多的样本再进下一档。
- 记三个数每档记录成功率、平均耗时和报错类型。第一个成功率明显下滑的档位,就是你实际的可承受并发布点。
- 做对照同一档位下分别测短请求和长请求,看槽位是被谁占掉的。长请求单独占池往往能让短请求成功率立刻回升。
- 核口径到 /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 开始逐档往上加,记录成功率和耗时,取成功率仍在可接受范围、且留有一定余量的那一档。上游额度、模型大小、请求长度都会影响这个值。
流式输出会不会更容易触发限制?
会更早占满并发槽位,因为一条流式连接保持时间更长,同样的并发数下同时在途的请求更多。所以流式场景的并发上限通常要比非流式设得更保守。
被限流之后流量怎么恢复?
先停止新增请求,让退避队列自然消化;确认成功率回升后再逐步放量,不要一次恢复到原速率。同时核对是配置问题还是额度问题,必要时改用备用渠道。