漏桶核心是请求驱动而非ticker驱动:allow()调用时惰性计算漏水量,仅用atomic维护lastleaktime和water,rate换算为整数纳秒粒度,严格拒绝超容请求;rate.limiter是令牌桶,非漏桶,二者语义与适用场景不同。

漏桶不是靠 ticker 驱动的
很多人一想到“漏”,就自然写个 time.Ticker 每 100ms 扣一次水位——这会导致高并发下大量 goroutine 竞争、调度开销暴涨,且水位更新与请求判断不同步,容易漏判或误判。漏桶真正的核心是「请求驱动」:每次 Allow() 调用时,才按时间差惰性计算已漏水量。
正确做法只维护两个原子字段:lastLeakTime(纳秒时间戳)和 water(当前水量,单位为「千分之一请求」等整数粒度),所有运算用 int64 + atomic 完成,不锁、不 sleep、不浮点累积误差。
-
rate必须换算为「每纳秒漏多少整数单位」,例如 100 QPS →ratePerNanosecond = 100 * 1e6(即每纳秒漏 0.0001 个请求,放大 1e6 倍存为整数) - 每次
Allow()先atomic.LoadInt64读出旧值,再用当前time.Now().UnixNano()算 elapsed,再算应漏量,最后atomic.CompareAndSwapInt64更新;失败则重试 - 拒绝逻辑必须严格:若
newWater + 1 > capacity(加 1 是因为本次请求要进桶),直接返回false,不修改状态
别把 rate.Limiter 当漏桶用
golang.org/x/time/rate.Limiter 是令牌桶,不是漏桶。它允许突发:只要桶里有 100 个令牌,哪怕刚 refill 过,也能立刻放行 100 次请求;而漏桶要求「每秒最多漏出 100 个」,无论请求怎么来,输出节奏绝对匀速。
如果你在 API 网关里用 rate.NewLimiter(100, 1) 替代漏桶,结果可能是前 50ms 就打满 100 QPS,后 950ms 完全空转——这违背了漏桶「平滑整形」的本意。
- 漏桶适用场景:保护下游数据库写入、限流日志上报、硬件设备通信等对输出抖动零容忍的链路
- 令牌桶适用场景:用户 API 接口限流、前端防刷,需要一定弹性应对合法突发
- 两者不可通过调参互相模拟;
rate.Limiter.Reserve()返回的是延迟建议,不是拒绝信号,语义完全不同
容量与速率单位必须对齐
漏桶的 capacity 和 rate 不是独立参数,它们共同决定「最大排队等待时长」:理论上最长可等 capacity / rate 秒。如果设 capacity=10、rate=100(QPS),那最大积压只有 0.1 秒,几乎不排队;若设 capacity=1000、rate=10,则可能积压 100 秒——这在 HTTP 请求里早已超时。
线上部署前必须按业务 SLA 反推:比如下游 DB 写入要求 P99
- HTTP 场景推荐
capacity ≤ rate × 0.3(即最多积压 300ms) - 后台任务队列可放宽,但需配合上游 timeout 控制,避免无限堆积
- 所有计算路径必须用整数:
water存为int64,单位是「万分之一请求」;elapsed用纳秒,leaked = elapsed × ratePerNanosecond / 1e6,全程无 float64
并发安全必须靠 atomic,不是 mutex
用 sync.Mutex 包裹整个 Allow() 逻辑看似简单,但在万级 QPS 下会成为性能瓶颈——锁竞争导致 goroutine 频繁挂起唤醒,CPU 大量耗在调度而非业务上。
真正高性能的漏桶只依赖 sync/atomic:把 water 和 lastLeakTime 都声明为 int64 字段,所有读写走 atomic.LoadInt64/atomic.CompareAndSwapInt64。CAS 失败就重试,无锁无阻塞。
- 不要试图用 struct 包裹多个字段再原子操作——Go 不支持 multi-field CAS,必须拆成单字段独立管理
-
lastLeakTime必须用纳秒时间戳(time.Now().UnixNano()),不用time.Time对象,避免内存分配和比较开销 - 初始化时用
atomic.StoreInt64(&l.lastLeakTime, time.Now().UnixNano()),别用time.Time{}零值
漏桶最难的不是算法,而是把「匀速流出」这个约束,严丝合缝地映射到原子操作和整数运算里——任何一步引入浮点、锁或定时器,都会让它在高并发下失准或变慢。











