流量控制
LLM API 限流
LLM 厂商(如 OpenAI、Anthropic、Google、Groq 等)为了保护后端模型服务器不被打崩,几乎都采用了双重限流策略:
- RPM(Requests Per Minute):每分钟最多允许发起多少个请求(Request)
- TPM(Tokens Per Minute):每分钟总共允许处理的 Token 数量(输入 + 输出 Token 之和)
即使你并发开 100 个请求,只要总 Token 数或请求数超过限制,就会立刻返回 HTTP 429 Too Many Requests,并且账户可能被短暂封禁(几秒到几分钟不等,严重时会更久)。
为什么需要同时限制 RPM 和 TPM?
- RPM 主要防止“请求风暴”(短时间内疯狂发很多小请求)。
- TPM 主要防止“Token 消耗爆炸”(一个请求输入 + 输出就几十万 Token,把模型算力吃光)。
两者必须同时控制,否则很容易被绕过。
常用的两种限流算法实现
1. 令牌桶算法(Token Bucket) —— 最常用、最推荐
核心思想:
- 系统有一个“桶”,里面预先装满了令牌(token)。
- 每发起一个请求,就从桶里拿走相应数量的令牌。
- 令牌会按照固定速率(rate)持续生成(比如每分钟生成 10,000 个 TPM 令牌)。
- 如果桶里没有足够的令牌,就拒绝请求(429)。
优点:
- 允许一定的突发流量(桶满了可以一次性消费)。
- 实现简单,性能高。
- 天然支持 RPM + TPM 双重限流(用两个桶分别控制)。
实际实现方式:
- 单机:用一个带时间衰减的计数器 + 原子操作。
- 分布式:推荐用 Redis + Lua 脚本 实现原子性。
伪代码示例(Redis 令牌桶):
-- Lua 脚本(原子执行)
local rpm_key = KEYS[1]
local tpm_key = KEYS[2]
local now = tonumber(ARGV[1])
local rpm_limit = tonumber(ARGV[2])
local tpm_limit = tonumber(ARGV[3])
local request_tokens = tonumber(ARGV[4]) -- 本次请求预计消耗的 token 数
-- RPM 检查(每个请求消耗 1 个)
local rpm_tokens = redis.call('GET', rpm_key) or rpm_limit
if rpm_tokens < 1 then
return 0 -- RPM 超限
end
-- TPM 检查
local tpm_tokens = redis.call('GET', tpm_key) or tpm_limit
if tpm_tokens < request_tokens then
return 0 -- TPM 超限
end
-- 扣减
redis.call('DECR', rpm_key)
redis.call('DECRBY', tpm_key, request_tokens)
-- 设置过期时间(通常 60 秒)
redis.call('EXPIRE', rpm_key, 60)
redis.call('EXPIRE', tpm_key, 60)
return 1 -- 允许通过
实际生产中会配合令牌自动补充逻辑(用当前时间计算应该补充多少令牌)。
2. 漏桶算法(Leaky Bucket)
核心思想:
- 把所有请求放入一个“漏桶”中。
- 漏桶以固定速率向外漏水(处理请求)。
- 如果桶满了,后续请求直接被丢弃或排队。
特点:
- 流量输出非常平滑,适合对后端压力要求极稳定的场景。
- 不允许突发,即使有令牌也不能一次性消费太多。
- 在 LLM 场景中使用较少(因为 OpenAI 本身允许一定突发)。
3. 滑动窗口算法(Sliding Window) —— 精度最高
核心思想:
- 记录最近 60 秒内每一个请求发生的时间和消耗的 Token 数。
- 每次请求时,计算当前窗口内(过去 60 秒)的总请求数和总 Token 数是否超过限制。
优点:
- 限流最精确,不会出现令牌桶那种“窗口交界处突然放行大量请求”的问题。
- 符合大多数 LLM 厂商实际的限流逻辑(他们通常就是滑动窗口或近似滑动窗口)。
缺点:
- 内存消耗较高(需要存很多时间戳 + token 数)。
- 需要用 Redis + Lua 或 RedisTimeSeries 等实现高性能。
推荐实现(分布式高精度):
- 用 Redis Sorted Set(zset)存储请求时间戳 + token 消耗。
- 每次请求时:
- 清理窗口外(60 秒前)的数据。
- 计算当前窗口内总请求数和总 Token 数。
- 判断是否超限。
- 插入当前请求记录。
实际开发建议
- 同时维护两个限流器:
- 一个 RPM 限流器(每个请求消耗 1)
- 一个 TPM 限流器(根据 prompt_tokens + max_tokens 或实际返回的 total_tokens 动态扣)
- 预估 Token 消耗:
- 请求前用 tiktoken / anthropic tokenizer 估算输入 Token。
- 请求后用 API 返回的 usage 字段精确扣除输出 Token。
- 指数退避 + 重试(Exponential Backoff):
- 遇到 429 时,等待 retry-after 头指定的时间,或用 1s → 2s → 4s → … 退避。
- 分布式环境强烈推荐 Redis + Lua:
- 保证原子性,避免竞态条件。
- 支持多实例、多进程共享限流。
- 更高级做法:
- 结合 自适应限流:根据当前剩余配额动态调整并发度。
- 使用 优先级队列:重要请求走高优先级桶。
- 做 多账户轮询(多 Key 负载均衡)时,每个账户独立做令牌桶。
总结
| 算法 | 实现难度 | 突发流量支持 | 精度 | 推荐场景 | 分布式支持 |
|---|---|---|---|---|---|
| 令牌桶 | 低 | 优秀 | 高 | 大多数 LLM 应用(推荐) | 优秀 |
| 漏桶 | 中 | 差 | 高 | 需要极致平滑流量 | 良好 |
| 滑动窗口 | 高 | 一般 | 最高 | 对精度要求极高场景 | 优秀 |