4. 漏桶算法详解

漏桶算法(Leaky Bucket)是限流领域最直观的算法之一。它将请求比作水滴,系统是一个底部有固定大小漏洞的桶。无论进水(请求)的速率如何波动,出水(处理请求)的速率始终保持恒定。这种“强制平滑”的特性,使其在需要严格保护后端服务的场景中不可替代。

4.1 算法工作机制

漏桶算法的核心思想是“恒定速率消费”。其工作流程分为三个阶段:

  1. 入桶:每个请求到达时,首先检查桶是否已满。如果桶未满,请求被放入桶中(等待处理);如果桶已满,请求被直接丢弃(限流)。
  2. 缓冲:桶作为一个先进先出(FIFO)队列,临时存储突发请求。队列深度决定了系统能容忍的最大突发流量。
  3. 漏出:系统以固定的速率从桶底“漏出”请求进行处理。这个速率是恒定的,不受入水速率影响。
关键区别: 与令牌桶算法不同,漏桶算法不关心“是否有令牌”,只关心“桶是否满了”以及“漏出的速度”。它强制输出为恒定速率,而令牌桶允许一定程度的突发。
漏桶算法工作机制流程图 请求流入 漏桶(FIFO队列) 请求A(等待中) 请求B(等待中) 请求C(等待中) 漏口 恒定速率处理 (如 10 req/s) 请求丢弃 桶满 核心特征: 输出速率恒定,强制平滑

4.2 参数配置详解

漏桶算法有两个核心参数,直接决定了限流效果和系统表现:

参数名称 配置项 说明 典型值
漏出速率 leakRate 单位时间内从桶底漏出的请求数量,即系统处理请求的恒定速率。 10 req/s、100 req/min
队列深度 queueDepth 桶的最大容量,即允许在队列中等待的最大请求数。超过此数目的请求将被丢弃。 50、200、1000
配置建议: 队列深度不宜过大。过深的队列会导致请求等待时间过长,增加尾部延迟(Tail Latency)。建议根据业务允许的最大等待时间(如 500ms)和漏出速率反推:queueDepth = maxWaitTime × leakRate。

4.3 适用场景

漏桶算法在以下场景中表现优异:

  • 保护下游脆弱系统:当后端服务(如数据库、第三方API)无法处理突发流量,必须强制平滑请求时,漏桶是最佳选择。
  • 流量整形(Traffic Shaping):需要将不规则的入站流量整形为恒定速率的出站流量,例如视频流、日志上报。
  • 削峰填谷:在秒杀、抢购等场景中,将瞬间的流量高峰削平,以恒定速率处理,避免系统过载。
  • 网络带宽控制:限制网卡或连接池的出口带宽,确保不会超过链路容量。

4.4 优缺点分析

维度 优点 缺点
输出稳定性 输出速率绝对恒定,后端压力可预测,系统稳定性极高。 无法应对突发流量,即使后端有能力处理更多请求,也会被强制限流。
实现复杂度 算法逻辑简单,只需维护一个计数器和一个定时器,实现成本低。 队列深度和漏出速率需要精确调优,配置不当容易导致大量请求被误杀。
资源利用率 在流量平稳时,资源利用率较高。 在流量低谷时,系统资源可能闲置;在流量高峰时,大量请求被丢弃,用户体验差。
适用性 非常适合保护脆弱系统、流量整形等场景。 不适合需要允许突发、追求高吞吐的场景(如缓存刷新、批量任务)。
避坑指南: 不要将漏桶算法用于“允许短时间突发”的业务场景。例如,用户刷新首页时,期望立即看到结果,而不是等待队列。此时应优先考虑令牌桶算法。

4.5 代码示例:漏桶算法实现

以下是一个基于 Go 语言的漏桶算法核心实现,展示了参数配置和限流逻辑:

package main

import (
    "fmt"
    "sync"
    "time"
)

// LeakyBucket 漏桶限流器
type LeakyBucket struct {
    rate       float64   // 漏出速率(请求/秒)
    capacity   int64     // 队列深度(桶容量)
    water      int64     // 当前水量(队列中的请求数)
    lastLeakMs int64     // 上次漏水时间戳(毫秒)
    mu         sync.Mutex
}

// NewLeakyBucket 创建漏桶实例
func NewLeakyBucket(rate float64, capacity int64) *LeakyBucket {
    return &LeakyBucket{
        rate:       rate,
        capacity:   capacity,
        water:      0,
        lastLeakMs: time.Now().UnixMilli(),
    }
}

// Allow 判断是否允许请求通过
func (lb *LeakyBucket) Allow() bool {
    lb.mu.Lock()
    defer lb.mu.Unlock()

    now := time.Now().UnixMilli()
    // 1. 先漏水:根据时间差计算漏出的水量
    elapsed := now - lb.lastLeakMs
    leakAmount := int64(float64(elapsed) / 1000.0 * lb.rate)
    if leakAmount > 0 {
        if leakAmount > lb.water {
            lb.water = 0
        } else {
            lb.water -= leakAmount
        }
        lb.lastLeakMs = now
    }

    // 2. 判断桶是否已满
    if lb.water < lb.capacity {
        lb.water++  // 加水(请求入桶)
        return true // 允许通过
    }

    // 桶满,丢弃请求
    return false
}

func main() {
    // 配置:漏出速率 10 req/s,队列深度 50
    bucket := NewLeakyBucket(10, 50)

    // 模拟请求
    for i := 0; i < 100; i++ {
        if bucket.Allow() {
            fmt.Printf("请求 %d: 通过\n", i)
        } else {
            fmt.Printf("请求 %d: 被限流\n", i)
        }
        time.Sleep(50 * time.Millisecond)
    }
}

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

在实际架构选型中,漏桶和令牌桶是最常被对比的两种算法。下表总结了它们的核心差异:

对比维度 漏桶算法 令牌桶算法
输出速率 恒定速率,强制平滑 允许突发,平均速率受限
应对突发 不擅长,突发请求被缓冲或丢弃 擅长,可累积令牌应对突发
实现机制 基于队列 + 定时漏出 基于令牌生成 + 桶存储
适用场景 保护脆弱系统、流量整形 允许突发、高吞吐场景
参数配置 漏出速率、队列深度 令牌生成速率、桶容量
漏桶 vs 令牌桶:架构对比 漏桶算法 突发请求流入 FIFO队列 (缓冲/丢弃) 恒定速率输出 输出:强制平滑,无突发 令牌桶算法 令牌生成器 令牌桶 (存储令牌) 允许突发输出 输出:可突发,平均速率受限

4.7 总结

漏桶算法通过“恒定速率漏出”的机制,为系统提供了最严格的流量保护。在模型验证 API 的限流熔断配置中,它特别适用于以下情况:

  • 后端模型推理服务无法承受任何突发流量(如 GPU 显存受限、批处理队列固定)。
  • 需要精确控制 API 的出口带宽,避免对下游依赖造成冲击。
  • 业务允许请求在队列中等待,且能接受一定的尾部延迟。

配置时需重点权衡队列深度与等待时间,避免因队列过长导致请求超时或资源浪费。