4. 限流算法(中):漏桶算法与令牌桶算法

在模型 Serving 层,限流算法的选择直接决定了流量整形效果与资源利用率。本节深入剖析两种最经典的限流算法——漏桶算法(Leaky Bucket)与令牌桶算法(Token Bucket),从原理、实现到适用场景进行完整对比。

4.1 漏桶算法(Leaky Bucket)

4.1.1 核心原理

漏桶算法将请求视为水滴,系统是一个底部有固定速率漏水孔的桶。无论上游请求多么突发,桶以恒定速率向下游处理单元漏水;当桶满时,新请求被丢弃或排队等待。

  • 恒定输出速率:无论输入流量如何波动,输出始终为固定速率。
  • 突发吸收能力:桶容量允许短时突发被缓存,但不会加速处理。
  • 强制整形:适用于需要严格平滑流量的场景,如数据库写入、下游慢服务保护。
关键指标:漏桶算法由两个参数决定——桶容量(burst)和漏水速率(rate)。桶容量决定了能容忍的瞬时突发量,漏水速率决定了系统的最大吞吐。

4.1.2 实现方式

漏桶的常见实现有两种:队列式(FIFO 队列 + 定时消费)和计数器式(记录上次漏水时间 + 当前水量)。以下为计数器式伪代码:

class LeakyBucket:
    def __init__(self, capacity, leak_rate):
        self.capacity = capacity          # 桶容量(最大水量)
        self.leak_rate = leak_rate        # 漏水速率(水滴/秒)
        self.water = 0                    # 当前水量
        self.last_leak_time = time.now()  # 上次漏水时间

    def allow_request(self, drop=1):
        now = time.now()
        # 计算从上次漏水到现在漏掉的水量
        elapsed = now - self.last_leak_time
        leaked = elapsed * self.leak_rate
        self.water = max(0, self.water - leaked)
        self.last_leak_time = now

        # 尝试加入水滴
        if self.water + drop <= self.capacity:
            self.water += drop
            return True   # 允许请求
        else:
            return False  # 桶满,拒绝请求

4.1.3 适用场景与局限

维度 说明
适用场景 下游处理能力固定、不允许突发加速的场景(如慢查询保护、硬件接口限速)
优势 输出绝对平滑,实现简单,内存占用低
局限 无法应对突发流量(即使系统有空闲资源),可能导致请求排队超时
避坑提示:在模型 Serving 层,如果下游推理服务支持短时突发(如 GPU 批处理加速),漏桶算法会强制削平突发,降低吞吐效率。此时应优先考虑令牌桶算法。
漏桶算法原理示意图 漏桶算法(Leaky Bucket)原理 上游请求(突发输入) 请求 A 请求 B 请求 C 桶(容量 = 5) 水滴 1 水滴 2 水滴 3 漏水孔 恒定输出(rate) 桶满丢弃

4.2 令牌桶算法(Token Bucket)

4.2.1 核心原理

令牌桶算法以固定速率向桶中放入令牌,每个请求必须获取一个或多个令牌才能被处理。桶有最大容量,允许积累令牌以应对突发流量。与漏桶不同,令牌桶允许在桶满时以峰值速率消费令牌,从而支持突发。

  • 允许突发:桶中积累的令牌可被一次性消耗,支持短时高吞吐。
  • 长期限速:令牌生成速率决定了长期平均速率。
  • 灵活控制:每个请求可消耗多个令牌,实现权重限流。
最佳实践:在模型 Serving 中,令牌桶算法非常适合 GPU 推理场景——平时积累令牌,当批处理窗口到达时一次性消耗多个令牌,充分利用 GPU 并行能力。

4.2.2 实现方式

令牌桶的经典实现使用时间差计算,避免定时器开销:

class TokenBucket:
    def __init__(self, capacity, fill_rate):
        self.capacity = capacity          # 桶容量(最大令牌数)
        self.fill_rate = fill_rate        # 令牌生成速率(令牌/秒)
        self.tokens = capacity            # 当前令牌数(初始满桶)
        self.last_fill_time = time.now()  # 上次填充时间

    def allow_request(self, tokens=1):
        now = time.now()
        # 计算从上次填充到现在应生成的令牌数
        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

        # 检查是否有足够令牌
        if self.tokens >= tokens:
            self.tokens -= tokens
            return True   # 允许请求
        else:
            return False  # 令牌不足,拒绝请求

4.2.3 适用场景与局限

维度 说明
适用场景 允许突发、需要平滑长期速率的场景(如 API 网关、模型推理服务)
优势 支持突发流量,资源利用率高,参数调节灵活
局限 突发可能导致下游瞬时过载,需要配合熔断机制
令牌桶算法原理示意图 令牌桶算法(Token Bucket)原理 令牌生成器(rate) 令牌桶(容量 = 10) T T T T T T 请求(消耗令牌) 请求通过(处理) 令牌不足拒绝

4.3 漏桶 vs 令牌桶:核心对比

对比维度 漏桶算法 令牌桶算法
输出特性 绝对平滑,恒定速率 允许突发,长期平滑
突发处理 吸收突发但不加速输出 积累令牌后允许突发输出
资源利用率 低(空闲资源无法利用) 高(可充分利用空闲资源)
实现复杂度 简单 中等
模型 Serving 推荐度 低(削峰导致吞吐下降) 高(匹配 GPU 批处理特性)
选型建议:在模型 Serving 层,令牌桶算法是更优选择。它允许在请求低谷时积累令牌,在请求高峰时以峰值速率处理,与 GPU 批处理、异步推理等机制天然契合。漏桶算法更适合下游处理能力严格固定的场景,如硬件设备限速或数据库连接池保护。