RPM、TPM 与并发:中转站限流的四种真实形态,以及怎么压出真实上限
限流为什么比报错更难查
报错至少会告诉你「出事了」。限流不会 —— 它有四种形态,其中三种不返回错误码。
| 形态 | 表面现象 | 实际发生的事 |
|---|---|---|
| 一、硬 429 | 请求被拒,429 Too Many Requests | 网关计数器满了,直接拒绝 |
| 二、静默排队 | 请求成功,但延迟从 2 秒涨到 60 秒 | 请求进了队列,等前面的空位 |
| 三、静默降级 | 请求成功,返回 200 | 超限后被路由到更便宜的上游或更小的模型 |
| 四、中途断流 | 流式响应提前结束,没有 [DONE] | 连接被主动切断,或上游配额耗尽 |
第二种最容易被误判成「网络慢」,第三种最容易被误判成「模型变笨」,第四种最容易被误判成「客户端 bug」。
这三种误判的共同后果是:你去优化了错误的地方。
三个独立的维度
限流不是「一个上限」,而是三个独立的上限,任一触发即生效:
- RPM(requests per minute):每分钟请求数,对短请求敏感;
- TPM(tokens per minute):每分钟 token 数,对长上下文敏感 —— 一个 100K 上下文的请求,可能一次就打满整分钟的配额;
- 并发连接数:同时在飞的请求数。它和 RPM 完全独立:你可以 RPM 很低但并发很高,只要每个请求都很慢。
这就是「我明明没超 RPM 却被限」经常发生的原因 —— 你超的是 TPM 或并发,而中转站的后台只显示 RPM。
阶梯压测:唯一能测出真实上限的方法
不要一上来就压 50 并发。你会同时触发所有限制,拿到一条无法解释的曲线。
正确的做法是阶梯加压,每档跑 60 秒,记录四个数字:成功率、P50 延迟、P95 延迟、首字延迟。
import time, statistics, concurrent.futures as cf, urllib.request, json
BASE, KEY, MODEL = "https://<base_url>", "<key>", "<model>"
def one():
body = json.dumps({"model": MODEL, "max_tokens": 64,
"messages": [{"role": "user", "content": "Reply with the single word: ok"}]}).encode()
req = urllib.request.Request(BASE + "/v1/chat/completions", data=body,
headers={"Authorization": "Bearer " + KEY, "Content-Type": "application/json"})
t0 = time.time()
try:
with urllib.request.urlopen(req, timeout=120) as r:
r.read()
return (time.time() - t0, r.status)
except Exception as e:
return (time.time() - t0, getattr(e, "code", 0))
for conc in (1, 2, 4, 8, 16):
lat, codes = [], []
with cf.ThreadPoolExecutor(max_workers=conc) as ex:
deadline, futs = time.time() + 60, []
while time.time() < deadline:
futs.append(ex.submit(one))
time.sleep(0.5) # 固定到达速率,避免「突发」污染结果
for f in cf.as_completed(futs):
d, c = f.result(); lat.append(d); codes.append(c)
ok = sum(1 for c in codes if c == 200) / len(codes)
print(conc, "ok=%.0f%%" % (ok * 100), "p50=%.1fs" % statistics.median(lat),
"p95=%.1fs" % sorted(lat)[max(0, int(len(lat) * 0.95) - 1)],
"429=", codes.count(429))
要点有三条:
- 固定到达速率(上面是每 0.5 秒一个),否则你测的是「突发抗性」,不是「持续上限」;
- 每档足够长(60 秒),短于 30 秒的测试会被瞬时配额掩盖;
- 先测单并发基线。基线自己就不稳(P95 抖动超过 2 倍),后面的数据全部无效。
四类曲线的判读
| 曲线形状 | 判读 | 下一步 |
|---|---|---|
| P50 平稳,无 429,P95 无拐点 | 还没到上限 | 继续加压,直到出现拐点 |
| P50 随并发线性上升,无 429 | 静默排队 | 你已经在限流里了,只是它不报错 |
| 某个并发点后 429 突增 | 硬限流点 | 这就是真实 RPM 上限 |
| P50 平稳但成功率下降(空响应、断流) | 中途断流 | 查上游配额或网关连接数上限 |
第三行是唯一「友好」的情况,因为你看得见。前两行才是绝大多数人踩的坑。
怎么问清分级规则
中转站的限流几乎都是按用户等级给的:充值档位、是否实名、是否企业客户,直接决定 RPM 和并发。
要问的四个问题,建议按这个顺序:
- 限流是按 RPM、TPM 还是并发算?三个都给数值最好。
- 超限时返回什么?429 还是排队? 如果对方说「我们不会限流」,基本等于「超了会排队」。
- 限流是每 Key 还是每账号?多 Key 能不能叠加?
- 晚高峰(20:00~23:00)的配额与白天是否一致?
第 4 条尤其关键。很多站白天一切正常,晚上 429 频发 —— 不是你的量涨了,是共享池被更多人分了。
一个反直觉的结论
限流阈值低不是问题,「阈值不透明加上超限静默」才是问题。
一家明说「你的档位是 60 RPM,超了返回 429」的站,你可以写重试和队列;一家说「我们不限流」但超限后把请求排队 40 秒的站,你连监控该看什么指标都不知道 —— 因为延迟上涨可以有无穷多种解释。
所以在选型清单里,「能否明确说出限流口径」应该单独占一项,和可用率同级。
小结
- 限流有四种形态,只有硬 429 会告诉你出事了;
- RPM、TPM、并发是三个独立维度,超的往往不是你以为的那个;
- 阶梯加压(1、2、4、8、16,每档 60 秒,固定到达速率)才能测出真实上限;
- 先测单并发基线,基线不稳则全部数据无效;
- P50 线性上升而没有 429,就是静默排队,这已经是限流状态;
- 选型时把「限流口径是否透明」当独立指标,比阈值高低更重要。
这篇文章对你有帮助吗?如果你在实际使用中发现了文中没提到的情况,欢迎 提交纠错或补充,我们会核验后更新到正文里。
相关文章
顺着看下去:本站收录的部分中转站
本文没有指向特定模型系列,下面是本站收录站点中综合评分靠前的几家,供你顺着看下去。排序依据是评分与可用率,不构成推荐。