4. 限流算法(中):漏桶算法原理、令牌桶算法原理、两种算法的优缺点与选型建议

在上一节中,我们介绍了计数器算法的实现与边界问题。本节将深入讲解两种在模型服务中应用最广泛的平滑限流算法:漏桶算法与令牌桶算法。理解它们的核心原理、数学特性以及适用场景,是设计高可靠模型服务限流架构的基础。

4.1 漏桶算法原理

漏桶算法(Leaky Bucket)的核心思想是将突发请求流“削峰填谷”,强制以恒定速率处理请求。无论外部请求到达的速率如何波动,经过漏桶后,流出的请求速率始终是固定的。

核心机制:请求先进入一个容量有限的队列(桶),然后以固定的速率从桶中取出并处理。当桶满时,新到达的请求被直接丢弃(拒绝服务)。

4.1.1 漏桶算法工作流程

  1. 请求入桶:每个请求到达时,检查当前桶中积压的请求数。
  2. 桶满判断:如果积压数已达到桶容量上限,则拒绝该请求(返回 429 或 503)。
  3. 恒定漏出:后台以固定的速率(如每秒 100 个请求)从桶中取出请求并转发给下游模型服务。
  4. 平滑输出:无论输入流量如何突发,输出流量始终是平滑的恒定速率。
模型服务场景:当模型推理服务对并发请求数有严格上限(如 GPU 显存限制),且要求请求处理间隔均匀时,漏桶算法是最直接的选择。例如,单张 A100 卡同时只能处理 4 个推理请求,漏桶可以确保每秒最多只有 4 个请求进入推理引擎。

4.1.2 漏桶算法示意图

漏桶算法原理示意图 突发请求流 速率波动大 入桶 漏桶 容量 = N 请求积压队列 恒定速率漏出 平滑输出流 恒定速率 桶满 → 丢弃

4.2 令牌桶算法原理

令牌桶算法(Token Bucket)是目前业界使用最广泛的限流算法之一。与漏桶的“强制恒定”不同,令牌桶允许一定程度的突发流量,同时保证长期平均速率不超过阈值。

核心机制:系统以固定速率向桶中放入令牌,每个请求必须从桶中获取一个或多个令牌才能被处理。如果桶中有足够的令牌,请求可以立即通过;如果令牌不足,请求等待或丢弃。

4.2.1 令牌桶算法工作流程

  1. 令牌生成:后台以恒定速率(如每秒 100 个)向桶中添加令牌,桶容量有上限(最大突发量)。
  2. 请求取令牌:每个请求到达时,尝试从桶中获取 1 个令牌(或根据请求权重获取多个)。
  3. 通过或等待:如果桶中有足够令牌,请求立即通过,令牌数减少;如果令牌不足,请求被拒绝或排队等待。
  4. 突发吸收:当桶中积累了大量令牌(如空闲期),可以一次性处理多个突发请求,但不会超过桶容量上限。
模型服务场景:当模型服务需要容忍短时流量突发(如用户秒杀场景),同时保证长期平均 QPS 不超过模型承载能力时,令牌桶是最佳选择。例如,模型支持平均 100 QPS,但允许在 1 秒内突发处理 200 个请求(桶容量=200)。

4.2.2 令牌桶算法示意图

令牌桶算法原理示意图 令牌生成器 r 个/秒 放入令牌 令牌桶 容量 = 最大突发 请求到达 取令牌 请求通过 令牌足够 令牌不足 → 拒绝

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 资源。
  • 中间件/消息队列:使用漏桶算法,平滑消费速率,避免下游处理能力不足。

这种分层组合策略既能吸收外部突发,又能保护内部资源,是大型模型服务系统的常见设计模式。