4. 漏桶算法详解
漏桶算法(Leaky Bucket)是限流领域最直观的算法之一。它将请求比作水滴,系统是一个底部有固定大小漏洞的桶。无论进水(请求)的速率如何波动,出水(处理请求)的速率始终保持恒定。这种“强制平滑”的特性,使其在需要严格保护后端服务的场景中不可替代。
4.1 算法工作机制
漏桶算法的核心思想是“恒定速率消费”。其工作流程分为三个阶段:
- 入桶:每个请求到达时,首先检查桶是否已满。如果桶未满,请求被放入桶中(等待处理);如果桶已满,请求被直接丢弃(限流)。
- 缓冲:桶作为一个先进先出(FIFO)队列,临时存储突发请求。队列深度决定了系统能容忍的最大突发流量。
- 漏出:系统以固定的速率从桶底“漏出”请求进行处理。这个速率是恒定的,不受入水速率影响。
关键区别: 与令牌桶算法不同,漏桶算法不关心“是否有令牌”,只关心“桶是否满了”以及“漏出的速度”。它强制输出为恒定速率,而令牌桶允许一定程度的突发。
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 令牌桶:核心对比
在实际架构选型中,漏桶和令牌桶是最常被对比的两种算法。下表总结了它们的核心差异:
| 对比维度 | 漏桶算法 | 令牌桶算法 |
|---|---|---|
| 输出速率 | 恒定速率,强制平滑 | 允许突发,平均速率受限 |
| 应对突发 | 不擅长,突发请求被缓冲或丢弃 | 擅长,可累积令牌应对突发 |
| 实现机制 | 基于队列 + 定时漏出 | 基于令牌生成 + 桶存储 |
| 适用场景 | 保护脆弱系统、流量整形 | 允许突发、高吞吐场景 |
| 参数配置 | 漏出速率、队列深度 | 令牌生成速率、桶容量 |
4.7 总结
漏桶算法通过“恒定速率漏出”的机制,为系统提供了最严格的流量保护。在模型验证 API 的限流熔断配置中,它特别适用于以下情况:
- 后端模型推理服务无法承受任何突发流量(如 GPU 显存受限、批处理队列固定)。
- 需要精确控制 API 的出口带宽,避免对下游依赖造成冲击。
- 业务允许请求在队列中等待,且能接受一定的尾部延迟。
配置时需重点权衡队列深度与等待时间,避免因队列过长导致请求超时或资源浪费。