写批量任务或者给上游接口做包装时,最容易纠结的一件事就是:到底该用信号量卡住并发数,还是丢进队列慢慢跑,还是只做个限流?三者听起来都在管并发,但管的层次和失效方式完全不同,选错了要么白占资源、要么照样被上游打回 429。这篇按判断标准来拆,不讲协议也不讲客户端配置,只讲工程上怎么选、怎么验证。
写批量任务、或者给上游接口做一层包装的时候,最容易纠结的就是这个:到底该用一个信号量卡住并发数,还是丢进队列慢慢跑,还是只做个限流就行?三者听起来都在管并发,但管的层次和失效方式其实完全不同,选错了要么白占着资源不动,要么照样被上游打回 429。
这篇不讲协议也不讲客户端配置,只讲工程上怎么选、选完怎么验证。核心思路是先分清每种方法到底在管什么,再对着自己的瓶颈去套,最后用一组检查动作确认没有踩坑。数据类的信息,比如某个平台的具体限制口径,去站内对应页面看当时的数字,这里只给判断标准。
先分清三者在管什么
| 方法 | 管的是什么 | 超限时的表现 | 主要代价 |
|---|---|---|---|
| 信号量 | 同时在跑的数量 | 阻塞或立即返回失败 | 慢任务长期占住许可 |
| 队列 | 先后顺序与缓冲 | 积压变长、延迟升高 | 延迟转嫁给调用方 |
| 限流 | 单位时间放行速率 | 直接拒绝 | 本身不解决资源竞争 |
三者不是互相替代的关系,常见做法是限流放在最外层、队列在中间、信号量放在消费端。限流负责挡住自己内部逻辑出 bug 时把上游打爆,队列负责削峰和保顺序,信号量负责守住本地那几个真正有限的资源。分不清层次的时候,先问一句:我到底在保护谁。
- 要控制本地资源占用,看信号量;
- 要削峰、要保顺序、要失败重投,看队列;
- 要防止上游被打爆、要兜底,看限流;
- 三层可以叠加,但参数要分开调,不要用一个数字同时管三件事
信号量适用与不适用的场景
信号量最适合的场景是调用方自己就是资源瓶颈:本地只有固定数量的连接、固定大小的显存、固定的工作线程,多开也没用,这时候用一个信号量把并发压住是最直接的。任务执行时间短且比较均匀时也合适,因为排队带来的调度开销反而比直接跑还高。
它不适用于任务耗时差异极大的场合。慢任务会长期占着许可不释放,其他请求全堵在门外,现象上看像卡死,实际上是许可没还回来。这种情况要么给许可加超时强制释放,要么改成队列按权重调度。需要保证先后顺序时也别用它,信号量不提供任何顺序保证,先拿到许可的先跑。
用之前先确认两件事
- 确认瓶颈是不是本地的如果并发开大了只是变慢、不是报错,那多半瓶颈在本地;如果一开大就被上游拒绝,那瓶颈在上游,该用限流而不是信号量
- 确认任务时长是否均匀统计一批任务的耗时分布,如果最长和最短差出量级,信号量方案要加超时释放,否则一定会堵
队列适用与不适用的场景
队列适合上游有明确速率上限、而你的任务本身可以等的情况。它把突发的量摊平,避免瞬间把并发打满。批处理类任务也适合进队列,因为队列天然能记录待处理、处理中、已失败三种状态,失败重投和积压观测都有地方挂。
它不适用于交互式请求。用户已经在等回复,进队列等于把延迟直接转嫁给用户,这种场景宁可快速失败也不要让用户干等。任务有强时效性时也要小心,积压久了再执行结果已经没意义,这种情况要在入队时就带上过期时间,过期直接丢弃而不是继续跑。
- 可等待任务
- 用户不在等结果、或者结果晚一点出来也没影响,才适合进队列
- 积压长度
- 队列必须暴露这个数,不然出问题时看不出是消费慢还是生产太快
- 过期时间
- 有强时效性的任务,入队时就要带,避免过期任务还占着消费能力
限流的边界与配合方式
限流分两种口径:按请求次数和按 token 或数据量。很多平台两种都卡,只看请求数会低估实际消耗,具体某个平台卡哪种口径、阈值多少,去站内对应页面对照当时的数字。落地时要记住,限流是兜底的第一道闸,不是主要的控制手段。
限流本身不解决资源竞争。如果瓶颈是本地资源,只加限流还是会堆满;反过来,只加信号量也挡不住突发速率。另外一个常见错误是限流触发后原地立即重试,这等于自己制造第二波流量,正确做法是退避重试或者降级返回。
选完之后的验证清单
方案定下来之后,别急着上量,先按下面几步验证一遍。第一步是量延迟基线,没有基线时任何并发数字都是猜的,可以先到站内的测速工具把当前链路跑一遍。第二步是压边界,把并发调到刚好触发上游拒绝,观察是信号量先卡住还是限流先返回,确认到底哪一层在起作用。
- 压一次边界并发逐步上调到刚好被拒,记录哪一层先触发,这决定了后续该调哪个参数
- 检查失败路径手动让一个任务超时,看许可有没有被释放、队列消息会不会重复消费、重试有没有上限
- 补观测字段至少记录同时在跑数量、队列积压长度、被拒次数,缺这三个数后面调参只能靠感觉
- 基线:先有延迟和成功率数据,再谈并发参数;
- 边界:确认哪一层先触发拒绝,只调那一层;
- 失败:超时、重试、重复消费这三条路径都要手动走一遍;
- 观测:同时在跑数、积压长度、被拒次数,三个都要有
常见问题
信号量和限流能不能只留一个?
如果瓶颈在上游速率,只留限流够用;如果瓶颈在本地资源,只留限流不够,因为限流不限制同时在跑的数量。反过来只留信号量,遇到突发的高速率请求仍然会瞬间打满。两者管的维度不同,多数生产链路是限流在外层兜底、信号量在消费端控制资源占用,中间再用队列削峰。
为什么并发设得很低还是报 429?
多半是口径没对齐:你数的是同时在跑的请求数,上游卡的可能是单位时间内的请求次数或 token 消耗量,也可能是账号级别的共享额度。先确认上游到底卡哪种,再看是不是同一个账号被多个服务共用。按额度分档的处理方式可以到站内相关页面看具体的排队与退避做法。
队列积压越来越多该怎么判断是不是设计问题?
看两件事:一是消费速度是否稳定,消费端忽快忽慢通常意味着内部还有别的瓶颈;二是任务是否有时效,如果积压的任务过期后跑完也没用,那就是入队时没设过期时间。持续单向增长一般不是参数问题,而是消费能力小于生产速度,加机器或砍任务量比调队列参数更有效。