golang.org/x/time/rate.limiter 不是漏桶,而是基于时间戳推算最早放行时间的滑动窗口式变体,允许突发;真漏桶应选 go.uber.org/ratelimit 或手写,需用原子操作维护水量与漏出时间,严格按 sla 反推 capacity 和 rate。

golang.org/x/time/rate.Limiter 不是漏桶,别当漏桶用
它底层不维护水位或漏出量,而是基于时间戳推算“最早能放行的时间”,行为更接近滑动窗口式漏桶变体,但允许突发——Allow() 返回 true 后,下 10 次调用可能全在 1ms 内通过,完全不平滑。
常见误判现象:
- 以为
rate.NewLimiter(100, 1)能把流量压成严格 100 QPS 匀速输出,实际前 10ms 就打满 100 次,后面 990ms 空转 - 在日志上报、DB 写入等对抖动敏感的链路里用它做“整形”,结果下游依然被脉冲打垮
- 试图通过增大
burst参数来延长排队等待时间,但它根本不排队,只是放宽延迟容忍窗口
要用真漏桶,选 go.uber.org/ratelimit 或手写 LeakyBucket
go.uber.org/ratelimit 是目前最贴近 RFC 定义的漏桶实现:每次 Take() 都阻塞到该漏出的时刻,输出节奏绝对恒定;它不提供“拒绝”接口,只做“整形”,语义清晰。
手写漏桶的关键约束:
- 必须用
atomic.LoadInt64和atomic.CompareAndSwapInt64维护water(当前水量)和lastLeakTime(纳秒时间戳),不能用锁或 float64 累加 -
rate要换算为「每纳秒漏多少整数单位」,例如 100 QPS →ratePerNanosecond = 100 * 1e6(放大 1e6 倍避免浮点) - 每次
Allow()必须惰性重算漏水量:delta := (now - lastLeakTime) * ratePerNanosecond / 1e9,再更新状态 - 拒绝逻辑要严格:
if newWater + 1 > capacity就直接return false,不修改任何字段
漏桶的 capacity 和 rate 必须按业务 SLA 反推
漏桶不是越大越好。capacity 和 rate 共同决定最大积压时长:maxWait = capacity / rate。这个值一旦超 HTTP timeout,请求就永远卡在桶里。
典型配置建议:
- HTTP API 限流(P99 ≤ 300ms):
capacity ≤ rate × 0.3,比如 100 QPS →capacity ≤ 30 - 日志上报通道(可容忍短时堆积):
capacity = rate × 5,但上游必须设好 timeout,防止 goroutine 泄漏 - 硬件串口通信(零抖动要求):
capacity = 1,rate 对齐设备处理能力,宁可丢也不缓存
用 time.Ticker 定时补漏是错的——它无法响应请求密度变化,高并发下会严重漏判。
HTTP 中间件里漏桶必须在读 body 前完成判断
漏桶逻辑一定要放在 http.Handler 最开头,且必须在 r.Body 读取之前、w.WriteHeader() 之前执行。否则:
- 先读 body 再限流,大文件上传会耗尽内存才被拒绝
- 限流失败后已调用
w.WriteHeader(),再写错误响应会触发http: multiple response.WriteHeader calls - 用
Reserve()或Take()返回的延迟时间做 sleep,必须确保没写 header,否则连接会卡住
真正难的不是算法本身,而是把「恒定流出速率」这个语义,严丝合缝地嵌进你整个请求生命周期里——从连接建立、header 解析、body 流式读取,到最终响应写出,每一步都得对齐漏桶节奏。稍一松懈,漏桶就变成摆设。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











