3、漏桶算法原理

漏桶算法(Leaky Bucket)是流量整形与限流领域最经典的算法之一。它通过一个固定容量的“桶”和恒定速率的“漏水”行为,将任意速率、任意突发程度的输入流量,平滑为固定速率的输出流量。理解漏桶的工作机制、与令牌桶的差异以及适用场景,是构建可靠限流骨架的基础。

3.1 工作机制

漏桶算法的核心思想可以用一个底部有漏洞的水桶来类比:

  • 桶(Bucket):一个固定容量的容器,用于暂存请求或数据包。桶的容量决定了系统能够容忍的最大突发量。
  • 漏水速率(Leak Rate):桶底部漏洞的流出速率,即系统处理请求的恒定速率。无论桶内积压了多少请求,漏出的速率始终固定。
  • 流入(Inflow):外部请求以任意速率注入桶中。如果桶未满,请求被暂存;如果桶已满,新请求被丢弃(拒绝服务)。
  • 流出(Outflow):桶以恒定速率将请求漏出,交由后端处理。
关键约束:漏桶算法强制输出速率 ≤ 漏水速率。即使输入瞬间爆发,输出也绝不会超过预设的恒定阈值。这是漏桶与令牌桶最本质的区别。

下图展示了漏桶算法的完整工作流程:

漏桶算法工作机制流程图 漏桶算法工作机制 任意速率请求 漏桶 容量:C 当前水位:W 剩余空间:C-W 已积压请求 漏口 恒定速率 R 平滑输出 桶满丢弃 请求被拒绝 核心特性 • 输出速率恒定 • 突发被缓存或丢弃 • 无令牌累积机制

3.2 与令牌桶算法的区别

漏桶算法与令牌桶算法是限流领域最常被对比的两种算法。虽然它们都用于控制流量速率,但设计哲学和适用场景截然不同。下表从多个维度进行对比:

对比维度 漏桶算法 令牌桶算法
核心机制 请求进入固定容量桶,以恒定速率漏出 以恒定速率生成令牌存入桶,请求需消耗令牌
输出速率 严格恒定,不会超过漏水速率 允许突发,只要桶内有足够令牌
突发处理 突发被缓存(桶内积压),超量丢弃 允许一定程度的突发(桶内令牌累积)
空闲行为 桶逐渐排空,空闲后输出降为零 桶逐渐填满令牌,空闲后仍可应对突发
流量整形效果 强整形,输出完全平滑 弱整形,允许短时流量尖峰
实现复杂度 较低,只需维护水位和计时器 中等,需维护令牌生成和桶状态
典型应用 网络流量整形、音视频流控 API限流、微服务熔断保护
记忆口诀:漏桶是“强制匀速”,令牌桶是“允许超车”。漏桶像收费站严格限速,令牌桶像加油站允许提前储备。

下图直观对比了两种算法在相同输入下的输出行为差异:

漏桶与令牌桶输出对比 漏桶 vs 令牌桶:输出行为对比 输入请求 时间轴 → 漏桶输出 恒定速率,间隔均匀 令牌桶输出 允许突发,输出密集 蓝色:正常请求 | 红色:突发请求 | 绿色:漏桶平滑输出 | 橙色:令牌桶突发输出

3.3 适用场景

漏桶算法在以下场景中具有明显优势:

  • 网络流量整形(Traffic Shaping):在路由器、交换机中,漏桶用于将突发的网络数据包平滑为恒定速率,避免拥塞。例如,ATM网络中的漏桶整形。
  • 音视频流媒体传输:视频流需要恒定比特率(CBR)传输,漏桶可以保证数据包以固定间隔发送,防止播放端缓冲溢出或欠载。
  • 数据库连接池限流:当数据库连接池的处理能力固定时,漏桶可以确保请求以不超过连接池处理能力的速率进入,避免连接池被突发请求打满。
  • 硬件资源保护:对于磁盘I/O、GPU计算等具有固定处理能力的硬件资源,漏桶可以防止突发请求导致硬件过载。
  • 消息队列消费速率控制:当消费者处理能力固定时,漏桶可以控制消息投递速率,避免消费者被压垮。
避坑提示:漏桶算法不适合需要应对突发流量的场景。例如,电商秒杀、热点新闻推送等场景,如果使用漏桶,突发流量会被大量丢弃,导致用户体验急剧下降。这类场景应优先考虑令牌桶算法。

3.4 Go语言实现要点

在Go中实现漏桶算法时,通常有两种方式:

  1. 基于时间戳的计数器实现:记录上次漏水时间和当前水位,每次请求到达时计算应漏水量,更新水位。
  2. 基于定时器的队列实现:使用一个固定长度的队列,定时器以固定速率从队列头部取出请求处理。

以下是一个基于时间戳的漏桶限流器核心代码:

package leakybucket

import (
    "sync"
    "time"
)

type LeakyBucket struct {
    rate       float64       // 漏水速率(请求/秒)
    capacity   float64       // 桶容量
    water      float64       // 当前水位
    lastLeakMs int64         // 上次漏水时间戳(毫秒)
    mu         sync.Mutex
}

func NewLeakyBucket(rate, capacity float64) *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()
    elapsed := float64(now - lb.lastLeakMs) / 1000.0 // 转为秒

    // 计算这段时间内漏掉的水量
    leaked := elapsed * lb.rate
    lb.water = lb.water - leaked
    if lb.water < 0 {
        lb.water = 0
    }

    lb.lastLeakMs = now

    // 判断桶是否已满
    if lb.water < lb.capacity {
        lb.water++
        return true
    }
    return false
}
实现要点:
  • 每次请求到达时先计算时间差,按速率漏水,再判断桶是否已满。
  • 使用互斥锁保证并发安全。
  • 漏水计算使用浮点数,注意精度问题;高并发场景可改用整数纳秒计算。
  • 桶容量和速率应根据后端处理能力合理设置,避免过大导致内存积压。