漏桶限流选用 time.ticker 而非 time.sleep,因其不阻塞 goroutine,能配合 channel 实现匀速令牌发放;桶大小由 channel 容量控制,溢出令牌自动丢弃;需通过 stopch 和 sync.once 确保 ticker 可停止防泄漏;中间件中须共享桶实例,拒绝时返回 429 并设置 retry-after。

漏桶限流为什么选 time.Ticker 而不是 time.Sleep
因为 time.Sleep 会阻塞 goroutine,而 Web 接口通常每请求启一个 goroutine,用它做限流等于把并发压成串行,吞吐直接崩盘。漏桶本质是“匀速放水”,time.Ticker 提供稳定、非阻塞的周期信号,配合 channel 可自然实现令牌发放节奏。
- 每次请求来时,尝试从令牌 channel 中
select非阻塞取一个令牌;取不到就拒绝 -
time.Ticker在后台持续往 channel 塞令牌,速率由time.Second / rate控制 - channel 容量即桶大小(burst),超过容量的令牌会被丢弃,符合漏桶“溢出即丢”的语义
如何避免 goroutine 泄漏导致内存暴涨
后台 ticker goroutine 必须可停止,否则服务长期运行后,反复 reload 或配置变更会累积大量僵尸 goroutine。漏桶实例应支持 Stop(),且 Web handler 中不能直接启动未托管的 ticker。
- 用
sync.Once确保 ticker 只启一次,用stopCh控制退出 - 在
http.Handler的ServeHTTP方法里不做新 goroutine 启动,所有 ticker 统一在服务初始化时创建 - 若用 struct 封装漏桶,字段必须含
stopCh chan struct{}和once sync.Once,Stop()方法中 close(stopCh) 并等待 ticker goroutine 退出
net/http 中间件里怎么安全嵌入漏桶逻辑
不能在中间件闭包里捕获外部变量做状态,比如把令牌 channel 写成闭包内局部变量——每个中间件实例都会新建一套,无法共享桶状态,等于没限流。
- 漏桶实例必须是全局单例或按路径/路由分组的共享对象,例如用
map[string]*LeakyBucket按pattern缓存 - 中间件函数签名保持为
func(http.Handler) http.Handler,内部通过闭包引用已初始化好的桶实例 - 拒绝时返回标准 HTTP 状态码:429 Too Many Requests,并设
Retry-After: 1(单位秒),方便前端退避 - 示例判断逻辑:
select { case
漏桶 vs 令牌桶:什么场景下你会踩坑
漏桶严格匀速输出,适合保护下游稳定性;但对突发流量零容忍。如果你的业务允许短时突增(比如秒杀预热),用漏桶反而误伤正常请求。
立即学习“go语言免费学习笔记(深入)”;
- API 响应延迟波动大时,漏桶会导致实际 QPS 远低于配置值——因为请求处理慢,令牌被后续请求“积压”消耗,桶空了就得等
- 没有重置机制:桶满后,重启服务或 reload 配置不会自动清空已积累的令牌,需显式调用
Reset()(内部重置 channel) - 调试时常见错误:
len(bucket.tokenCh)看似能查剩余令牌,但 channel len 不代表可用令牌数(可能正被其他 goroutine 读取中),应只用 select + default 判断
真正难的是根据 P95 延迟和下游扛压能力反推桶大小和速率,而不是写对那个 for-select 循环。










