2. 常见限流算法:原理与对比
在低延迟模型 API 的限流熔断设计中,限流算法是核心决策点。不同的算法在突发处理、平滑度、内存开销和实现复杂度上差异显著。本节深入剖析四种主流限流算法的原理、适用场景与性能特征。
2.1 计数器算法(Counter Algorithm)
计数器算法是最直观的限流实现。它将时间划分为固定窗口(如 1 秒),在每个窗口内维护一个计数器,请求到达时计数器加 1;当计数器达到阈值时,拒绝后续请求;窗口结束时重置计数器。
核心特征:实现简单,内存开销极低(仅需一个计数器和时间戳),但存在严重的“窗口边界突发”问题——在窗口切换瞬间可能允许两倍于阈值的请求通过。
// 计数器算法伪代码
class CounterLimiter:
limit = 100 // 每秒最大请求数
counter = 0 // 当前计数
windowStart = now() // 窗口起始时间
func allow():
if now() - windowStart >= 1s:
counter = 0
windowStart = now()
if counter < limit:
counter++
return true
return false
2.2 滑动窗口算法(Sliding Window Algorithm)
滑动窗口算法将固定窗口细分为多个小格子(如 1 秒窗口分为 10 个 100ms 格子),每个格子独立计数。窗口随着时间推移“滑动”,始终统计最近 N 个格子的总请求数,从而平滑边界效应。
性能提示:滑动窗口的精度由格子数量决定。格子越多,边界越平滑,但内存和计算开销线性增长。生产环境中通常将 1 秒分为 10~20 个格子,在精度与性能间取得平衡。
| 对比维度 | 计数器算法 | 滑动窗口算法 |
|---|---|---|
| 窗口边界突发 | 严重(可达 2 倍阈值) | 轻微(取决于格子数) |
| 内存开销 | O(1) | O(N)(N 为格子数) |
| 实现复杂度 | 低 | 中 |
| 适用场景 | 对突发不敏感、资源极受限 | 需要较精确限流、可接受少量内存 |
2.3 漏桶算法(Leaky Bucket Algorithm)
漏桶算法将请求视为水滴,以固定速率从桶底“漏出”处理;桶本身有容量上限,当桶满时新请求被丢弃。无论请求到达的速率如何波动,漏桶始终以恒定速率处理请求,实现完美的流量整形。
关键限制:漏桶算法无法应对突发流量——即使系统有能力处理瞬时高峰,漏桶也会强制将突发请求排队或丢弃。对于需要快速响应突发请求的低延迟 API,漏桶可能不是最佳选择。
2.4 令牌桶算法(Token Bucket Algorithm)
令牌桶算法以固定速率向桶中添加令牌,桶有容量上限。每个请求需要消耗一个令牌才能被处理。与漏桶不同,令牌桶允许一定程度的突发——当桶中积累了大量令牌时,可以一次性处理多个请求。
低延迟 API 首选:令牌桶算法在平滑限流与允许突发之间取得了最佳平衡,是生产环境中最常用的限流算法。它既能防止系统过载,又能利用空闲容量处理瞬时高峰。
// 令牌桶算法伪代码
class TokenBucketLimiter:
capacity = 100 // 桶容量
tokens = 100 // 当前令牌数
rate = 50 // 每秒生成令牌数
lastRefill = now() // 上次补充时间
func allow():
now = now()
elapsed = now - lastRefill
tokens = min(capacity, tokens + elapsed * rate)
lastRefill = now
if tokens >= 1:
tokens--
return true
return false
2.5 四种算法综合对比
| 算法 | 突发处理 | 流量整形 | 内存开销 | 实现复杂度 | 低延迟 API 推荐度 |
|---|---|---|---|---|---|
| 计数器 | 差(边界突发) | 无 | O(1) | 极低 | ★☆☆☆☆ |
| 滑动窗口 | 中(取决于格子数) | 弱 | O(N) | 中 | ★★★☆☆ |
| 漏桶 | 不允许 | 强(完全平滑) | O(1) | 低 | ★★☆☆☆ |
| 令牌桶 | 允许(桶容量内) | 中(平滑但允许突发) | O(1) | 低 | ★★★★★ |
选型建议:对于低延迟模型 API,推荐优先采用令牌桶算法作为核心限流策略。它既能通过令牌生成速率控制平均请求率,又能通过桶容量允许合理的突发请求,完美匹配模型推理场景中“平时低负载、偶尔批量请求”的流量特征。若业务要求严格的请求间隔(如避免同时打满 GPU 显存),可考虑在令牌桶外层叠加漏桶进行二次整形。