应使用golang.org/x/time/rate.limiter,limit设为每秒令牌数、burst至少等于limit(如10 qps配burst=10), burst过小导致首秒大量拒绝,过大则限流失效;按ip或用户需用sync.map隔离实例,字节流限速须改用juju/ratelimit。

直接用 golang.org/x/time/rate 就能稳住接口速率,不用 Redis、不写 Lua 脚本也能上线,但配错 Limit 和 Burst 会立刻翻车——比如设了 10 QPS 却连 3 次请求都放不过。
rate.Limiter 的 Limit 和 Burst 怎么设才不卡死
Limit 是每秒平均令牌数(单位:次/秒),Burst 是桶最大容量(单位:个),二者必须协同设置,不能只调大 Limit 就以为“变快了”。
- 刚把
Limit从 5 改成 10,下一秒调Allow()仍可能只成功 4 次——因为桶里 token 是按时间推算补的,不是瞬间重置;上一秒只攒了不到 10 个,还受Burst上限压制 -
Burst必须 ≥Limit,否则新速率永远达不到。例如Limit=10但Burst=5,桶最多存 5 个 token,再快也白搭 - 想扛突发流量,
Burst可设为Limit × 2或更高(如 10 QPS 配Burst=20);纯匀速场景可设为Limit相同值 - 别写
rate.Every(time.Second / 10)这种表达式——Go 编译器可能优化掉除法,应显式写rate.Limit(10)
HTTP 中间件里怎么安全嵌入 rate.Limiter
不能全局共用一个实例,也不能每个请求 new 一个,得按维度隔离 + 并发安全缓存。
- 单个
*rate.Limiter实例无法区分来源,硬套在中间件里会导致所有用户共享一桶,失去限流意义 - 用
sync.Map缓存不同 key 对应的限流器,key 推荐用清洗后的X-Forwarded-For(防代理污染),避免直接用c.Request.RemoteAddr - IPv6 地址含冒号,当 key 时需先标准化(如转为 IPv4-mapped 或哈希截断),否则
sync.Map键长失控 - 对
GET /health这类探活接口,务必绕过限流,否则健康检查失败会触发雪崩
Allow()、Reserve()、Wait() 到底该选哪个
三者都消费 token,但失败处理逻辑完全不同,选错会导致阻塞、超时或误拒。
-
Allow()纯判断:有 token 就返回true,没 token 立刻返回false,适合快速拒绝(如前端防重复提交) -
Reserve()预占:返回rate.Reservation,可查是否 OK、能等多久,适合需要自定义等待策略的场景(如带退避重试) -
Wait()阻塞等待:必须传带超时的context.Context,否则可能永久卡住;常见错误是传context.Background(),导致 goroutine 泄漏 - 若用
WaitN(ctx, n)控制字节流(如上传限速),n 必须是本次Write()的实际字节数,不能固定写 1
上传/下载限速为什么不能用 rate.Limiter
golang.org/x/time/rate 是为「请求频次」设计的(单位:次/秒),不是为「字节吞吐」设计的;混用会导致行为错乱。
- 用
rate.NewLimiter(1*1024*1024, 1*1024*1024)去限上传流——它把 1MB 当成“1048576 次请求”,不是“1MB/s”,结果一读就失败 - 字节级限速该用
github.com/juju/ratelimit:调ratelimit.NewBucketWithRate(fillRate, capacity),其中fillRate设目标速率(如1 * 1024 * 1024),capacity建议 ≥fillRate - 嵌入 I/O 时,必须用
bucket.WaitMaxDuration(n, timeout)控制每次Read()或Write(),且 timeout 要小于 HTTP 超时,否则连接卡死
最易被忽略的是:动态调速时,SetLimit() 和 SetBurst() 必须同时调,否则新 Limit 会被旧 Burst 卡住;还有就是 IP 限流不加 TTL 清理,sync.Map 会越积越大,最后 OOM。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











