rate.Limiter足够用,但每次请求new实例、硬编码路径、忽略并发竞争会导致限流失效;因每个实例维护独立令牌桶,100并发即100个桶,总流量失控;正确做法是全局复用或按key缓存并配过期清理。

rate.Limiter 足够用,但直接 new 实例、硬编码路径、忽略并发竞争,这三类写法会让限流形同虚设。
为什么每次请求都 new rate.Limiter 会失效
每个 rate.Limiter 实例维护独立的令牌桶状态。如果在 handler 里写 limiter := rate.NewLimiter(10, 5),每来一个请求就新建一个桶——100 个并发请求等于 100 个桶各自放行 10 QPS,实际总流量是 1000 QPS,完全失去控制。
- 正确做法是全局复用(如全局限流)或按 key 缓存(如 per-user、per-path)
- 缓存必须用
sync.Map或带sync.RWMutex的 map,避免高并发下 panic - key 命名要带区分维度,例如
"user:" + userID、"ip:" + realIP、"path:" + r.URL.Path - 别忘了清理闲置 key:对每个新 key 启动
time.AfterFunc(30 * time.Minute, func() { syncMap.Delete(key) })
Allow() 和 Wait() 怎么选、怎么用才安全
Allow() 是非阻塞判断,返回 false 后必须立刻中断后续逻辑;Wait(ctx) 虽能阻塞等待,但没设超时就会永久挂起。
- 网关层推荐用
Allow()快速拒绝,避免堆积 —— 写完if !limiter.Allow() { http.Error(...) }后必须return,否则请求仍会执行 - 若需等待(如后台任务),用
limiter.Wait(ctx),且 ctx 必须来自r.Context()并套一层context.WithTimeout(..., 100*time.Millisecond) - 别用
Allow()+time.Sleep模拟等待 —— 这既不阻塞也不限流,纯属误导 - 要返回标准限流头(
X-RateLimit-Limit等),得用limiter.ReserveN(now, 1)预占再算剩余,直接减法在并发下不准
多实例部署时 rate.Limiter 为啥不管用
它只在当前进程内存中生效。5 个 Pod 各自跑一个 rate.Limiter(100, 200),入口总流量就是 500 QPS —— 这不是 bug,是设计使然。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 跨实例限流必须依赖共享存储,
Redis是事实标准,etcd次之 - Redis 方案必须用 Lua 脚本封装
INCR+EXPIRE+GET,拆成多次网络调用会因竞态导致计数错误 - key 命名要细粒度,比如
"limit:svc:user-get:12345",而不是笼统的"user-get" - 连接池要调大(
PoolSize: 50),同时设置Timeout: 100ms,超时直接拒或放行,别让限流逻辑自己变成瓶颈
健康检查和配置热更新容易被忽略的坑
未跳过健康检查的限流中间件,会让 k8s probe 反复失败;配置变更时未加锁 reload limiter map,会导致 panic。
-
/health、/readyz等接口务必跳过限流,可在中间件开头加if strings.HasPrefix(r.URL.Path, "/health") { next.ServeHTTP(...) } - 从 etcd / YAML 动态加载规则时,用
sync.RWMutex保护 limiter map,读操作用RLock,写操作用Lock - 不要每次请求都解析 YAML 或调用 etcd Get —— 做带 TTL 的本地缓存 + watch 监听变更
- 未匹配到规则的路径默认不限流,但建议加个兜底全局限流器(如 1000 QPS),防配置遗漏
真正难的不是写通限流逻辑,而是 key 提取不漏(比如 X-Real-IP 没配 TrustedProxies 就拿不到真实 IP)、配置热加载不 panic、Redis 故障时有降级策略 —— 这些细节不处理好,上线后第一波流量就可能打穿。

















