漏桶算法详解

漏桶算法(Leaky Bucket)是流量整形与限流领域最经典的算法之一。其核心思想是将突发流量“削峰填谷”,强制输出速率恒定,非常适合需要严格平滑流量的场景,如催收系统的外呼速率控制、模型推理服务的请求排队等。

算法原理

漏桶算法将请求比作水滴,系统内部维护一个固定容量的“桶”。请求到达时,先尝试放入桶中;桶满则直接丢弃请求(拒绝服务)。桶底部有一个固定速率的“漏洞”,以恒定速率将请求漏出并处理。无论外部请求如何突发,经过漏桶后,输出到下游服务的流量始终是平滑的。

核心机制: 漏桶本质是一个先进先出(FIFO)队列 + 固定速率消费器。队列长度即桶容量,消费速率即漏出速率。
漏桶算法核心流程 漏桶算法核心流程 突发请求 漏桶(容量 = C) 请求 1 请求 2 请求 3 请求 4(排队中) 溢出丢弃 漏洞(速率 = R) 平滑输出 恒定速率 R

参数配置

漏桶算法有两个关键参数,直接决定限流效果与系统吞吐能力:

参数 符号 定义 配置建议(催收/Model Serving)
速率(Rate) R 桶漏水的固定速率,即系统每秒能处理的请求数(TPS)。 设为下游服务最大安全吞吐的 70%~80%,预留缓冲。
容量(Capacity) C 桶的最大存储量,即允许瞬时积压的最大请求数。 根据业务容忍的排队延迟设定。催收外呼建议 50~200;模型推理建议 100~500。
配置经验: 容量 C 越大,对突发流量的吸收能力越强,但排队延迟也越大。在催收场景中,若外呼任务有实时性要求(如 5 秒内必须发起),则 C 应 ≤ R × 5。

适用场景:平滑流量

漏桶算法最适合对输出流量平滑度有严格要求的场景,而非追求最大吞吐。典型应用包括:

  • 催收外呼速率控制: 防止瞬间大量外呼导致运营商封号或被叫方投诉,强制外呼速率恒定。
  • Model Serving 请求排队: GPU 推理服务无法承受突发高并发,漏桶可将请求均匀喂给模型,避免显存溢出(OOM)。
  • 数据库写入限流: 防止业务高峰期突发写入导致主从延迟或锁竞争。
  • 第三方 API 调用: 对方服务有严格的 QPS 配额,漏桶确保不超限。

与令牌桶对比

令牌桶(Token Bucket)是另一种主流限流算法,常与漏桶混淆。两者核心区别在于对突发流量的处理哲学:

对比维度 漏桶算法 令牌桶算法
核心思想 强制输出速率恒定,削峰填谷 允许一定程度的突发,但限制平均速率
突发处理 完全不允许突发输出,输出永远平滑 允许突发输出(最多消耗桶内所有令牌)
流量特征 输出流量严格平滑,无尖刺 输出流量可短时尖刺,但长期平均可控
实现机制 FIFO 队列 + 固定速率消费 令牌生成器 + 令牌桶 + 请求消耗令牌
适用场景 对下游有严格平滑要求的场景(如硬件设备、运营商线路) 允许短时突发、追求吞吐利用率的场景(如 Web 服务、API 网关)
空闲期影响 空闲期不积累“漏出能力”,恢复后仍按原速率输出 空闲期积累令牌,恢复后可应对突发流量
漏桶 vs 令牌桶流量对比 漏桶 vs 令牌桶:输出流量形态对比 漏桶输出(严格平滑) 恒定速率 R —— 无波动 突发请求被排队或丢弃,输出永远平坦 适合:催收外呼、GPU 推理、硬件驱动 令牌桶输出(允许突发) 突发 高 平稳 低 空闲积累令牌,短时突发可被吸收 适合:Web API、微服务、缓存穿透防护 选择依据: 下游能否承受短时尖刺?能 → 令牌桶;不能 → 漏桶
常见误区: 漏桶并非“越平滑越好”。过度平滑会导致请求排队时间过长,在催收场景中可能错过最佳联系时段。建议结合业务超时时间动态调整容量 C,或采用“漏桶 + 熔断”组合策略。

代码示例:漏桶限流器(Go 语言)

以下是一个基于时间戳的漏桶实现,适用于催收外呼或模型推理的请求排队:

package main

import (
    "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 := float64(now - lb.lastLeakMs)
    leakAmount := int64(elapsed * lb.rate / 1000)
    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
}
生产建议: 上述代码适用于单机限流。在分布式催收系统中,建议使用 Redis 实现分布式漏桶,将 water 和 lastLeakMs 存储在 Redis 中,配合 Lua 脚本保证原子性。