3. 令牌桶算法原理与实现
令牌桶算法是模型风险管理 API 限流中最常用的算法之一。它通过一个“桶”来平滑突发流量,同时保证长期平均速率不超过预设阈值。在模型推理、特征计算、批量评分等高频调用场景中,令牌桶能有效防止下游模型服务被瞬时流量冲垮。
3.1 工作机制
令牌桶的核心思想是:系统以固定速率向桶中放入令牌,每个请求必须从桶中获取一个(或多个)令牌才能被处理。如果桶中令牌不足,请求被拒绝或排队等待。
关键行为:
- 填充速率(rate):系统每秒向桶中添加的令牌数,决定了长期平均请求速率。
- 桶容量(capacity):桶最多能存放的令牌数,决定了允许的突发流量上限。
- 突发处理:当桶满时,新生成的令牌被丢弃;当请求到来时,若桶中有令牌则立即消耗,否则拒绝。
3.2 参数配置
令牌桶的两个核心参数直接影响限流效果。在模型风险管理场景中,需要根据模型推理耗时、并发数、SLA 要求进行调优。
| 参数 | 符号 | 含义 | 模型风险场景建议 |
|---|---|---|---|
| 速率(rate) | r | 每秒放入桶中的令牌数,即长期平均 QPS | 根据模型最大吞吐量 × 0.8 设置,预留缓冲 |
| 容量(capacity) | C | 桶的最大令牌数,决定突发流量承受能力 | 通常设为 rate × (1~3) 秒,避免短时尖峰打满 CPU |
| 当前令牌数 | tokens | 桶中实时可用令牌数,随时间动态变化 | 初始值 = capacity,启动时可立即处理突发 |
| 最后填充时间 | last_time | 上次更新令牌数的时间戳,用于计算增量 | 使用毫秒级精度,避免浮点误差累积 |
参数调优经验:对于模型推理 API,建议先通过压测获得单机最大 QPS,再将 rate 设为最大 QPS 的 70%~80%,capacity 设为 rate 的 2 倍。例如最大 QPS=1000,则 rate=800,capacity=1600。这样既能平滑流量,又能容忍 2 秒内的突发请求。
3.3 Python 代码实现
以下实现基于时间戳计算令牌增量,避免使用定时器,适合在模型推理中间件中嵌入。代码包含完整的参数校验、令牌获取和状态查询。
import time
import threading
class TokenBucket:
"""令牌桶限流器,用于模型风险管理 API 限流"""
def __init__(self, rate: float, capacity: int):
"""
初始化令牌桶
:param rate: 令牌填充速率(个/秒)
:param capacity: 桶容量(最大令牌数)
"""
if rate <= 0:
raise ValueError("rate 必须大于 0")
if capacity <= 0:
raise ValueError("capacity 必须大于 0")
self.rate = rate
self.capacity = capacity
self.tokens = capacity # 初始装满令牌
self.last_refill_time = time.time()
self.lock = threading.Lock() # 线程安全
def _refill(self):
"""计算并补充自上次填充以来新增的令牌"""
now = time.time()
elapsed = now - self.last_refill_time
# 新增令牌数 = 速率 × 经过时间
new_tokens = elapsed * self.rate
self.tokens = min(self.capacity, self.tokens + new_tokens)
self.last_refill_time = now
def try_acquire(self, tokens: int = 1) -> bool:
"""
尝试获取指定数量的令牌
:param tokens: 需要的令牌数(默认 1)
:return: True 表示获取成功,False 表示令牌不足
"""
if tokens <= 0:
raise ValueError("tokens 必须大于 0")
with self.lock:
self._refill()
if self.tokens >= tokens:
self.tokens -= tokens
return True
return False
def get_available_tokens(self) -> float:
"""获取当前可用令牌数(近似值)"""
with self.lock:
self._refill()
return self.tokens
def reset(self):
"""重置令牌桶为满状态"""
with self.lock:
self.tokens = self.capacity
self.last_refill_time = time.time()
# ========== 使用示例 ==========
if __name__ == "__main__":
# 配置:速率 100 QPS,容量 200(允许 2 秒突发)
bucket = TokenBucket(rate=100, capacity=200)
# 模拟请求
for i in range(10):
if bucket.try_acquire():
print(f"请求 {i+1}: 通过 ✅ (剩余令牌: {bucket.get_available_tokens():.1f})")
else:
print(f"请求 {i+1}: 限流 ❌")
time.sleep(0.01) # 模拟请求间隔 10ms
3.4 代码关键设计说明
- 惰性填充:只在获取令牌时计算时间差并补充令牌,避免后台线程开销,适合模型推理这种请求间隔不固定的场景。
- 线程安全:使用
threading.Lock保证多线程环境下令牌计数的准确性,防止超发。 - 浮点精度:
elapsed * self.rate在长时间运行时可能产生微小误差,但模型 API 限流场景下可忽略。若需高精度,可使用decimal.Decimal。 - 容量限制:
min(self.capacity, self.tokens + new_tokens)确保令牌数不超过桶容量,避免累积过多令牌导致突发流量失控。
模型风险特别提示:令牌桶算法虽然能平滑流量,但无法防止“慢请求堆积”。当模型推理耗时突然增加(如特征工程异常、模型退化),即使 QPS 在限流范围内,并发数也可能飙升。建议结合 信号量 或 最大并发数控制 一起使用,形成双层防护。