4、漏桶实现:基于 time.Ticker 的漏桶实现、自定义漏桶结构体

漏桶算法(Leaky Bucket)是限流领域最直观的流量整形模型之一。与令牌桶的“积攒令牌再消费”不同,漏桶强制请求以恒定速率流出,超出桶容量的请求直接丢弃。在 Go 语言中,利用 time.Ticker 实现漏桶是最贴近底层系统时钟的工程方案。

4.1 漏桶核心模型与结构体设计

自定义漏桶结构体需要封装三个核心属性:桶容量(burst)、恒定流出速率(rate)、以及用于驱动周期性流出的 time.Ticker。同时需要记录当前桶中的“水位”(pending),即等待处理的请求数。

字段 类型 说明
capacity int64 桶的最大容量,超出此值的请求被拒绝
rate time.Duration 每次漏出(处理)一个请求的时间间隔
ticker *time.Ticker 周期性触发漏出操作的定时器
pending int64 当前桶中等待处理的请求数(原子操作)
mu sync.Mutex 保护 pending 和 ticker 状态变更的互斥锁
设计要点: 使用 sync/atomic 操作 pending 字段可减少锁竞争;但启动/停止 ticker 时必须加锁,防止并发关闭导致 panic。

4.2 基于 time.Ticker 的漏桶实现

以下代码展示了完整的自定义漏桶结构体及其核心方法:NewLeakyBucket、Allow、StartDrain、Stop。

package leakybucket

import (
    "sync"
    "sync/atomic"
    "time"
)

// LeakyBucket 漏桶限流器
type LeakyBucket struct {
    capacity int64           // 桶容量
    rate     time.Duration   // 漏出间隔
    ticker   *time.Ticker    // 周期性漏出定时器
    pending  int64           // 当前等待数(原子操作)
    mu       sync.Mutex      // 保护 ticker 启停
    done     chan struct{}   // 通知 drain goroutine 退出
}

// NewLeakyBucket 创建一个漏桶限流器
// capacity: 桶容量;rate: 每次漏出的时间间隔
func NewLeakyBucket(capacity int64, rate time.Duration) *LeakyBucket {
    lb := &LeakyBucket{
        capacity: capacity,
        rate:     rate,
        done:     make(chan struct{}),
    }
    return lb
}

// StartDrain 启动后台漏出协程
func (lb *LeakyBucket) StartDrain() {
    lb.mu.Lock()
    defer lb.mu.Unlock()
    if lb.ticker != nil {
        return // 已经启动
    }
    lb.ticker = time.NewTicker(lb.rate)
    go func() {
        for {
            select {
            case <-lb.ticker.C:
                // 每次 tick 漏出一个请求
                for {
                    current := atomic.LoadInt64(&lb.pending)
                    if current == 0 {
                        break
                    }
                    if atomic.CompareAndSwapInt64(&lb.pending, current, current-1) {
                        break
                    }
                }
            case <-lb.done:
                return
            }
        }
    }()
}

// Allow 尝试向桶中放入一个请求
// 返回 true 表示放入成功(请求被接受),false 表示桶满(请求被丢弃)
func (lb *LeakyBucket) Allow() bool {
    for {
        current := atomic.LoadInt64(&lb.pending)
        if current >= lb.capacity {
            return false // 桶满,拒绝
        }
        if atomic.CompareAndSwapInt64(&lb.pending, current, current+1) {
            return true // 放入成功
        }
        // CAS 失败,重试
    }
}

// Stop 停止漏桶,关闭 ticker 和 drain 协程
func (lb *LeakyBucket) Stop() {
    lb.mu.Lock()
    defer lb.mu.Unlock()
    if lb.ticker != nil {
        lb.ticker.Stop()
        lb.ticker = nil
        close(lb.done)
    }
}
实现技巧: 漏出操作使用 for + CAS 循环,确保在高并发下 pending 递减的原子性。后台 drain goroutine 通过 done channel 安全退出,避免 goroutine 泄漏。

4.3 漏桶工作流程(内联 SVG)

下图展示了漏桶从请求入桶到恒定速率漏出的完整生命周期:

漏桶限流工作流程 请求到达 pending < capacity? 拒绝请求 pending++ 漏桶 pending 计数 time.Ticker pending-- 流程说明: 1. 请求到达后,检查 pending 是否小于 capacity。 2. 桶满则直接拒绝(红色分支);未满则原子递增 pending(绿色分支)。 3. time.Ticker 以恒定间隔触发漏出操作,每次 CAS 递减 pending。 4. 漏出速率 = 1 / rate(例如 rate=200ms 则每秒漏出 5 个请求)。

4.4 使用示例与边界场景

以下代码演示了如何创建漏桶、启动 drain 协程,并在 HTTP 处理函数中使用:

func main() {
    // 桶容量 10,每 100ms 漏出一个请求(即 10 QPS)
    lb := leakybucket.NewLeakyBucket(10, 100*time.Millisecond)
    lb.StartDrain()
    defer lb.Stop()

    http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
        if !lb.Allow() {
            http.Error(w, "429 Too Many Requests", http.StatusTooManyRequests)
            return
        }
        w.Write([]byte("OK"))
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}
边界场景注意:
  • rate 过小: 若 rate < 1ms,ticker 可能无法精确触发,建议最小间隔 1ms。
  • capacity 为 0: 所有请求都会被拒绝,需在业务层校验。
  • 并发 Stop: 多次调用 Stop() 会导致 close(done) panic,需用 sync.Once 保护。
  • drain 协程泄漏: 忘记调用 Stop() 会导致 goroutine 永久运行。

4.5 漏桶 vs 令牌桶:核心差异

对比维度 漏桶(Leaky Bucket) 令牌桶(Token Bucket)
流量特性 强制恒定速率输出 允许突发流量(积攒令牌)
实现核心 time.Ticker + pending 计数 定时填充 + 令牌计数
丢弃策略 桶满即丢(请求入桶时) 令牌不足时等待或丢弃
适用场景 数据库连接池、消息队列消费 API 网关、突发流量保护
Go 标准库支持 无,需自定义 golang.org/x/time/rate
审计建议: 在需要严格限制流出速率(如第三方 API 调用频率控制)时优先选择漏桶;在需要容忍短时突发(如用户秒杀场景)时选择令牌桶。生产环境中建议对 pending 和 capacity 添加 Prometheus 指标监控,便于观察限流水位。