漏桶算法详解
漏桶算法(Leaky Bucket)是流量整形与限流领域最经典的算法之一。其核心思想是将突发流量“削峰填谷”,强制输出速率恒定,非常适合需要严格平滑流量的场景,如催收系统的外呼速率控制、模型推理服务的请求排队等。
算法原理
漏桶算法将请求比作水滴,系统内部维护一个固定容量的“桶”。请求到达时,先尝试放入桶中;桶满则直接丢弃请求(拒绝服务)。桶底部有一个固定速率的“漏洞”,以恒定速率将请求漏出并处理。无论外部请求如何突发,经过漏桶后,输出到下游服务的流量始终是平滑的。
核心机制: 漏桶本质是一个先进先出(FIFO)队列 + 固定速率消费器。队列长度即桶容量,消费速率即漏出速率。
参数配置
漏桶算法有两个关键参数,直接决定限流效果与系统吞吐能力:
| 参数 | 符号 | 定义 | 配置建议(催收/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 网关) |
| 空闲期影响 | 空闲期不积累“漏出能力”,恢复后仍按原速率输出 | 空闲期积累令牌,恢复后可应对突发流量 |
常见误区: 漏桶并非“越平滑越好”。过度平滑会导致请求排队时间过长,在催收场景中可能错过最佳联系时段。建议结合业务超时时间动态调整容量 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 脚本保证原子性。