go中漏桶应按请求到达时动态计算水位,避免time.ticker;用atomic原子操作整数水位与纳秒时间戳,配合整数运算防浮点误差;rate.limiter是令牌桶而非漏桶,语义不同不可替代。

Go 里用 time.Ticker 实现漏桶容易卡死
漏桶不是靠“定时倒水”模拟的,而是靠请求到达时检查“桶里还有多少水位”,再决定是否放行。用 time.Ticker 持续扣减令牌会引入不必要的 goroutine 和状态竞争,且在低频请求下浪费 CPU。
- 正确做法是只在每次请求来临时计算「当前应剩余的令牌数」:用
time.Since(lastLeakTime)算出已漏掉多少,更新水位和时间戳 - 避免用
sync.Mutex锁整个判断逻辑——只要把water和lastLeakTime放进一个 struct,用sync/atomic原子读写int64时间戳 +float64水位(或全转成整数毫秒+整数令牌)更轻量 - 别直接用
float64存水位:浮点误差累积会导致某次突然多放一个请求,改用「剩余容量 = 最大容量 - 已消耗令牌数」,所有运算走整数
为什么 rate.Limiter 不是漏桶,别误用
golang.org/x/time/rate.Limiter 是令牌桶(token bucket),它允许突发流量(只要令牌够),而漏桶强制匀速输出。两者语义不同,不能简单替换。
- 漏桶核心约束是「单位时间最多流出 N 个请求」,不关心上游是否集中打过来;令牌桶则是「单位时间最多补充 N 个令牌,但可攒着一次性用完」
- 如果你要限流后端 API,要求每秒稳定处理 100 QPS、绝不抖动,就得自己实现漏桶;用
rate.NewLimiter(100, 1)可能前 10ms 就打满 100 次调用 -
rate.Limiter.Reserve()返回的*rate.Reservation含有延迟信息,但这属于令牌桶的预占机制,和漏桶“拒绝 or 立即通过”的二元决策无关
并发安全的漏桶结构体怎么写
关键字段只有三个:capacity(桶大小)、rate(每秒漏多少)、water(当前水量),但必须保证多 goroutine 同时调用 Allow() 时不冲突。
- 用
sync/atomic管理water和lastLeakTime:把时间戳存为int64(纳秒),水量也转为整数(例如单位是「千分之一请求」,避免小数) - 每次
Allow()先原子读当前水位和上次漏水时间,再算出应漏掉多少,再 CAS 更新——失败就重试,不用锁 - 示例片段:
func (l *LeakyBucket) Allow() bool { now := time.Now().UnixNano() prevWater := atomic.LoadInt64(&l.water) prevTime := atomic.LoadInt64(&l.lastLeakTime) elapsed := float64(now-prevTime) / 1e9 leaked := int64(elapsed * float64(l.rate)) newWater := maxInt64(0, prevWater-leaked) if newWater >= l.capacity { return false } if atomic.CompareAndSwapInt64(&l.water, prevWater, newWater+1) { atomic.StoreInt64(&l.lastLeakTime, now) return true } return l.Allow() // retry }
测试时发现漏桶吞吐量不对?检查时间精度和单位换算
本地跑测试常看到实际 QPS 偏高或偏低,问题大概率出在时间单位没对齐,比如把毫秒当秒用、或漏率按秒算却用纳秒差值直接乘。
立即学习“go语言免费学习笔记(深入)”;
- 统一用纳秒做时间差,除以
1e9得秒数;如果rate定义为「每秒漏 N 个」,那漏量 =(now - last) / 1e9 * rate - 测试时别用
time.Sleep(1 * time.Second)等待,系统调度不准;改用time.AfterFunc或记录起始时间后循环time.Since()判断 - 压测工具如
hey或ab自身有并发模型,结果反映的是「漏桶 + 客户端并发策略」共同效果,想验证纯算法,得写单 goroutine 循环调用并统计间隔
漏桶真正的复杂点不在逻辑,而在时间与整数的协同精度控制——少一次 1e9 换算,或者没处理好 atomic 的 ABA 问题,就会在高并发下悄悄漂移。










