2、API限流基础概念:限流的目的、限流与熔断的区别、常见限流算法
在模型风险管理系统中,API 是模型服务对外暴露的唯一窗口。当突发流量、恶意攻击或上游系统故障导致请求量激增时,若不加控制,模型服务可能因资源耗尽而崩溃,甚至引发级联故障。限流(Rate Limiting)与熔断(Circuit Breaking)是保障系统稳定性的两大核心手段,但二者在目标、触发机制和恢复方式上存在本质区别。
2.1 限流的目的
限流的核心目标是主动控制请求速率,确保系统在安全阈值内平稳运行。具体目的包括:
- 保护后端资源:防止模型推理服务因 CPU、内存、GPU 或数据库连接池耗尽而崩溃。
- 保障公平使用:避免某个调用方(如高频交易策略)独占资源,导致其他业务方被饿死。
- 抵御突发流量:平滑尖峰请求,防止瞬时流量冲垮系统。
- 成本控制:在云原生环境中,限流可避免因无限制调用导致的计算资源费用失控。
- 满足 SLA:通过限制并发数或 QPS,保证核心请求的响应时间在约定范围内。
模型风险场景提示: 在信用评分模型 API 中,若未限流,某合作方在月末批量调用时可能将 QPS 推至 5000+,导致模型服务超时、评分结果延迟,进而影响全行信贷审批流程。
2.2 限流与熔断的区别
限流和熔断经常被混淆,但它们在故障处理链路中扮演不同角色。下表从多个维度进行对比:
| 对比维度 | 限流(Rate Limiting) | 熔断(Circuit Breaking) |
|---|---|---|
| 核心目标 | 预防系统过载,主动控制请求速率 | 快速失败,防止故障扩散,给系统恢复时间 |
| 触发条件 | 请求速率超过预设阈值(如 QPS > 1000) | 错误率或延迟超过阈值(如 5xx 比例 > 50%) |
| 动作方式 | 直接拒绝、排队等待或降级处理 | 断开调用链路,返回默认值或快速失败 |
| 恢复机制 | 流量回落到阈值以下后自动恢复 | 需经过半开状态探测,确认服务健康后关闭熔断 |
| 关注指标 | 请求速率(QPS/TPS)、并发数 | 错误率、响应时间、慢调用比例 |
| 典型场景 | 秒杀、爬虫防护、API 配额管理 | 下游数据库宕机、模型推理异常飙升 |
实战建议: 在模型风险管理系统中,通常将限流作为第一道防线部署在网关层,熔断作为第二道防线部署在服务调用层。二者配合使用,可构建“先限流、后熔断”的纵深防御体系。
2.3 常见限流算法
不同的限流算法在精度、内存开销和应对突发流量的能力上各有优劣。以下三种算法是业界最常用的方案。
2.3.1 令牌桶算法(Token Bucket)
令牌桶算法以固定速率向桶中放入令牌,每个请求需获取一个令牌才能被处理。桶有最大容量,允许一定程度的突发流量。其核心参数包括:
- 令牌生成速率(r):每秒向桶中添加的令牌数,决定长期平均速率。
- 桶容量(b):桶中最多存放的令牌数,决定突发流量的大小。
- 当前令牌数(c):实时可用令牌数量。
算法流程:
- 系统以固定速率 r 向桶中添加令牌,桶满则丢弃多余令牌。
- 请求到达时,检查桶中是否有可用令牌(c > 0)。
- 若有,则消耗一个令牌,请求通过;若无,则请求被拒绝或排队。
适用场景: 模型 API 需要容忍短时突发调用(如实时风控在交易高峰期突然涌入),同时保证长期平均速率可控。令牌桶是业界最推荐的选择。
2.3.2 漏桶算法(Leaky Bucket)
漏桶算法将请求视为水滴,以固定速率从桶底漏出处理,桶满则溢出(拒绝)。与令牌桶不同,漏桶强制平滑流量,不允许突发。其核心参数包括:
- 漏出速率(r):每秒从桶底流出的请求数,即处理速率。
- 桶容量(b):桶中最多容纳的等待请求数。
- 当前水量(w):桶中正在等待的请求数。
注意: 漏桶算法对突发流量不友好。若模型 API 需要处理瞬时高峰(如开盘瞬间的量化交易请求),漏桶可能导致大量请求被直接丢弃,而令牌桶则能通过积累的令牌应对突发。
2.3.3 计数器算法(Counter / Fixed Window)
计数器算法是最直观的限流方式:将时间划分为固定窗口(如 1 秒),统计窗口内的请求数,超过阈值则拒绝。其核心参数为:
- 窗口大小(w):时间窗口长度,通常为 1 秒或 1 分钟。
- 窗口阈值(t):每个窗口内允许的最大请求数。
- 当前计数(c):当前窗口已累计的请求数。
临界问题: 计数器算法存在“窗口切换瞬间的流量尖刺”。例如窗口大小为 1 秒、阈值为 100,若第 0.9 秒有 100 个请求,第 1.0 秒又有 100 个请求,则 0.1 秒内实际通过了 200 个请求。滑动窗口或令牌桶可解决此问题。
2.3.4 三种算法对比
| 对比维度 | 令牌桶 | 漏桶 | 计数器(固定窗口) |
|---|---|---|---|
| 突发处理能力 | 支持(受桶容量限制) | 不支持 | 支持(但存在临界问题) |
| 平滑度 | 中等(允许突发后平滑) | 强制平滑 | 较差(窗口边界可能抖动) |
| 内存开销 | 低(仅维护令牌数和时间戳) | 低(仅维护队列长度) | 极低(仅维护一个计数器) |
| 实现复杂度 | 中等 | 低 | 极低 |
| 适用场景 | 通用场景,模型 API 首选 | 需要严格平滑的场景(如数据库写入) | 简单配额控制,对精度要求不高 |
| 模型风险推荐度 | ★★★★★ | ★★★☆☆ | ★★☆☆☆ |
选型建议: 在模型风险管理系统中,推荐优先使用令牌桶算法。它既能应对模型调用量的自然波动(如业务高峰),又能通过桶容量限制最大突发量,防止恶意攻击。若使用 Redis 实现分布式限流,令牌桶的 Lua 脚本方案已非常成熟。
2.3.5 代码示例:令牌桶算法核心逻辑
以下为令牌桶算法的简化实现,展示了核心的令牌消耗与补充逻辑:
import time
class TokenBucket:
def __init__(self, rate: float, capacity: int):
self.rate = rate # 令牌生成速率(个/秒)
self.capacity = capacity # 桶容量
self.tokens = capacity # 当前令牌数
self.last_refill = time.time() # 上次补充时间
def _refill(self):
"""按时间间隔补充令牌"""
now = time.time()
elapsed = now - self.last_refill
new_tokens = elapsed * self.rate
self.tokens = min(self.capacity, self.tokens + new_tokens)
self.last_refill = now
def try_acquire(self, count: int = 1) -> bool:
"""尝试获取 count 个令牌,成功返回 True"""
self._refill()
if self.tokens >= count:
self.tokens -= count
return True
return False
# 使用示例:每秒 100 个令牌,桶容量 200
bucket = TokenBucket(rate=100, capacity=200)
for i in range(300):
if bucket.try_acquire():
print(f"请求 {i}: 通过")
else:
print(f"请求 {i}: 限流")
time.sleep(0.005) # 模拟 5ms 间隔
生产环境注意: 上述代码为单机演示版本。生产环境建议使用 Redis + Lua 脚本实现分布式令牌桶,确保多实例间的限流一致性。同时需注意时钟回拨问题,建议使用单调时钟(
time.monotonic())而非系统时钟。