4. 限流算法(中):漏桶算法原理、令牌桶算法原理、两种算法的优缺点与选型建议
在上一节中,我们介绍了计数器算法的实现与边界问题。本节将深入讲解两种在模型服务中应用最广泛的平滑限流算法:漏桶算法与令牌桶算法。理解它们的核心原理、数学特性以及适用场景,是设计高可靠模型服务限流架构的基础。
4.1 漏桶算法原理
漏桶算法(Leaky Bucket)的核心思想是将突发请求流“削峰填谷”,强制以恒定速率处理请求。无论外部请求到达的速率如何波动,经过漏桶后,流出的请求速率始终是固定的。
核心机制:请求先进入一个容量有限的队列(桶),然后以固定的速率从桶中取出并处理。当桶满时,新到达的请求被直接丢弃(拒绝服务)。
4.1.1 漏桶算法工作流程
- 请求入桶:每个请求到达时,检查当前桶中积压的请求数。
- 桶满判断:如果积压数已达到桶容量上限,则拒绝该请求(返回 429 或 503)。
- 恒定漏出:后台以固定的速率(如每秒 100 个请求)从桶中取出请求并转发给下游模型服务。
- 平滑输出:无论输入流量如何突发,输出流量始终是平滑的恒定速率。
模型服务场景:当模型推理服务对并发请求数有严格上限(如 GPU 显存限制),且要求请求处理间隔均匀时,漏桶算法是最直接的选择。例如,单张 A100 卡同时只能处理 4 个推理请求,漏桶可以确保每秒最多只有 4 个请求进入推理引擎。
4.1.2 漏桶算法示意图
4.2 令牌桶算法原理
令牌桶算法(Token Bucket)是目前业界使用最广泛的限流算法之一。与漏桶的“强制恒定”不同,令牌桶允许一定程度的突发流量,同时保证长期平均速率不超过阈值。
核心机制:系统以固定速率向桶中放入令牌,每个请求必须从桶中获取一个或多个令牌才能被处理。如果桶中有足够的令牌,请求可以立即通过;如果令牌不足,请求等待或丢弃。
4.2.1 令牌桶算法工作流程
- 令牌生成:后台以恒定速率(如每秒 100 个)向桶中添加令牌,桶容量有上限(最大突发量)。
- 请求取令牌:每个请求到达时,尝试从桶中获取 1 个令牌(或根据请求权重获取多个)。
- 通过或等待:如果桶中有足够令牌,请求立即通过,令牌数减少;如果令牌不足,请求被拒绝或排队等待。
- 突发吸收:当桶中积累了大量令牌(如空闲期),可以一次性处理多个突发请求,但不会超过桶容量上限。
模型服务场景:当模型服务需要容忍短时流量突发(如用户秒杀场景),同时保证长期平均 QPS 不超过模型承载能力时,令牌桶是最佳选择。例如,模型支持平均 100 QPS,但允许在 1 秒内突发处理 200 个请求(桶容量=200)。
4.2.2 令牌桶算法示意图
4.3 两种算法的优缺点对比
漏桶与令牌桶虽然都实现了平滑限流,但在突发处理能力、输出特性、资源消耗等方面存在显著差异。下表从多个维度进行对比:
| 对比维度 | 漏桶算法 | 令牌桶算法 |
|---|---|---|
| 输出速率 | 严格恒定,完全平滑 | 允许短时突发,长期平均恒定 |
| 突发处理 | 不支持突发,桶满即丢弃 | 支持突发,桶中积累的令牌可一次性使用 |
| 空闲期行为 | 桶逐渐变空,无法积累“信用” | 桶逐渐填满,可积累令牌用于后续突发 |
| 实现复杂度 | 较低,只需队列+定时器 | 中等,需要令牌生成器+桶状态维护 |
| 内存开销 | 需要队列存储积压请求 | 只需存储令牌计数,无请求队列 |
| 适用场景 | 对输出抖动极度敏感的场景 | 需要容忍突发、兼顾平均速率控制的场景 |
| 模型服务典型用例 | GPU 推理引擎并发数严格限制 | API 网关、模型推理服务入口限流 |
4.4 选型建议
在模型服务架构中,选择漏桶还是令牌桶,需要根据下游服务的特性和业务需求来决定。以下是具体的选型建议:
重要提示:选型错误可能导致模型服务过载或资源利用率低下。例如,对 GPU 推理引擎使用令牌桶且突发量设置过大,可能导致显存 OOM;对 API 网关使用漏桶,则可能因无法处理突发流量而丢失大量用户请求。
4.4.1 推荐使用漏桶的场景
- GPU 推理引擎:模型推理通常需要固定大小的显存和计算资源,并发数超过上限会导致 OOM 或严重性能下降。漏桶的严格恒定输出可以确保并发数始终在安全范围内。
- 数据库连接池:模型服务与数据库之间的连接数有限,漏桶可以防止突发查询打满连接池。
- 硬件资源受限的推理节点:如边缘设备、嵌入式设备,CPU/内存资源固定,无法弹性扩展。
4.4.2 推荐使用令牌桶的场景
- API 网关/入口限流:用户请求天然具有突发性(如秒杀、热点事件),令牌桶可以吸收突发,同时保证长期平均 QPS 不超过模型承载能力。
- 多租户模型服务:不同租户的流量模式不同,令牌桶可以为每个租户分配独立的令牌桶,实现公平调度与突发容忍。
- 弹性伸缩的模型服务:当后端模型服务可以快速弹性扩容时,令牌桶允许短时超量请求,等待扩容完成。
- 需要区分请求权重的场景:令牌桶支持每个请求消耗多个令牌,适合对计算量不同的请求进行差异化限流(如大模型推理 vs 小模型推理)。
4.4.3 选型决策表
| 决策条件 | 推荐算法 | 原因 |
|---|---|---|
| 下游服务并发数有硬上限 | 漏桶 | 强制恒定输出,避免超限 |
| 需要容忍用户突发流量 | 令牌桶 | 积累令牌应对突发 |
| 请求处理时间差异大 | 令牌桶 | 支持按权重消耗令牌 |
| 资源无法弹性扩展 | 漏桶 | 严格保护下游资源 |
| 需要精确控制请求间隔 | 漏桶 | 输出间隔完全均匀 |
| 需要最大化资源利用率 | 令牌桶 | 空闲期积累令牌,忙时充分利用 |
4.5 代码示例:两种算法的核心实现
以下提供漏桶与令牌桶的简化实现代码,展示核心逻辑。实际生产环境中建议使用成熟的限流库(如 Guava RateLimiter、Sentinel 等)。
4.5.1 漏桶算法核心实现
import time
import threading
class LeakyBucket:
def __init__(self, capacity, leak_rate):
"""
:param capacity: 桶容量(最大积压请求数)
:param leak_rate: 漏出速率(请求数/秒)
"""
self.capacity = capacity
self.leak_rate = leak_rate
self.water = 0 # 当前桶中积压的请求数
self.last_leak_time = time.time()
self.lock = threading.Lock()
def _leak(self):
"""执行漏出操作,根据时间间隔减少桶中水量"""
now = time.time()
elapsed = now - self.last_leak_time
# 计算这段时间内应该漏出的请求数
leaked = elapsed * self.leak_rate
self.water = max(0, self.water - leaked)
self.last_leak_time = now
def try_acquire(self):
"""尝试获取处理资格,返回 True 表示允许通过"""
with self.lock:
self._leak()
if self.water < self.capacity:
self.water += 1
return True
else:
return False # 桶满,拒绝请求
4.5.2 令牌桶算法核心实现
import time
import threading
class TokenBucket:
def __init__(self, capacity, fill_rate):
"""
:param capacity: 桶容量(最大令牌数,即最大突发量)
:param fill_rate: 令牌填充速率(令牌数/秒)
"""
self.capacity = capacity
self.fill_rate = fill_rate
self.tokens = capacity # 初始时桶是满的,允许立即突发
self.last_fill_time = time.time()
self.lock = threading.Lock()
def _fill(self):
"""执行令牌填充操作"""
now = time.time()
elapsed = now - self.last_fill_time
new_tokens = elapsed * self.fill_rate
self.tokens = min(self.capacity, self.tokens + new_tokens)
self.last_fill_time = now
def try_acquire(self, tokens=1):
"""
尝试获取指定数量的令牌
:param tokens: 需要的令牌数(默认 1)
:return: True 表示获取成功,请求允许通过
"""
with self.lock:
self._fill()
if self.tokens >= tokens:
self.tokens -= tokens
return True
else:
return False # 令牌不足,拒绝请求
实现要点:
- 两种算法都使用 延迟计算 策略,避免使用定时器线程,降低实现复杂度。
- 漏桶的
water表示积压请求数,令牌桶的tokens表示可用令牌数,两者含义相反。 - 令牌桶初始化时桶满(
tokens = capacity),允许服务启动后立即处理突发请求。 - 生产环境中建议使用
time.monotonic()替代time.time(),避免系统时间调整导致限流失效。
4.6 模型服务中的选型实践
在实际的模型服务架构中,通常会在不同层级组合使用两种算法:
- 入口网关层:使用令牌桶算法,应对用户突发流量,保证 API 可用性。
- 模型推理引擎层:使用漏桶算法,严格限制并发推理请求数,保护 GPU/CPU 资源。
- 中间件/消息队列:使用漏桶算法,平滑消费速率,避免下游处理能力不足。
这种分层组合策略既能吸收外部突发,又能保护内部资源,是大型模型服务系统的常见设计模式。