rate.Limiter仅适用于单机限流,需按路径/IP/用户隔离实例,避免全局共享;高并发下应优先用Allow()返回429而非Wait()阻塞;跨节点必须依赖Redis+Lua实现原子计数,固定窗口用INCR+EXPIRE,滑动窗口用ZSET或Lua脚本。
rate.Limiter 做单机限流,但别只靠它Go 官方 golang.org/x/time/rate 提供的 rate.Limiter 是最常用、开箱即用的限流方案,适合保护单实例服务不被突发请求打垮。但它本质是内存态、无共享的——多个 Pod 或机器各自维护自己的桶,无法协同控流。
常见错误是直接在 HTTP handler 里全局初始化一个 limiter,然后对所有路径共用同一规则。这会导致:核心接口(如 /buy)和边缘接口(如 /health)被一视同仁,一旦限流触发,健康检查也可能失败,引发误判下线。
sync.Map 存 map[string]*rate.Limiter,key 可以是 "user:123" 或 "ip:192.168.1.100"
limiter.Wait(ctx) 长时间阻塞:它会挂起 goroutine 等令牌,高并发下可能堆积大量等待协程。改用 limiter.Allow() + 立即返回 429 更安全rate.NewLimiter(rate.Limit(100), 10) 表示「每秒最多 100 个请求,允许最多 10 个突发」,不是「10 秒内最多 100 个」微服务部署多实例后,单机限流就失效了。有人试图用 sync.Map 加定时广播或 channel 同步计数,结果要么数据不一致,要么引入严重延迟和锁竞争——这是典型踩坑。
真正可靠的做法是把窗口计数逻辑下沉到 Redis,并用原子 Lua 脚本保证操作不可拆分。例如固定窗口(Fixed Window)只需一行 INCR + EXPIRE,但要注意临界问题;滑动窗口(Sliding Window)则需用 Redis 的 ZSET 记录时间戳,成本更高但更精准。
INCR key + EXPIRE key 60
MaxActive: 20),否则限流中间件自己先成为瓶颈很多人以为 “Go 并发强 = 每个请求起一个 goroutine 就万事大吉”,结果压测时 P99 延迟飙升、GC 频繁、runtime.NumGoroutine() 突破万级——这不是并发强,是失控。
根本问题是:HTTP handler 默认为每个请求启动 goroutine,而网络 I/O、DB 查询、外部调用都可能阻塞,导致 goroutine 长期挂起,调度器不堪重负。这时候限流只是“堵上游”,goroutine 池才是“控下游”。
ants 或 goflow 替代裸 go handle(),池大小建议从 CPU 核心数 × 3 开始压测,而非盲目设 1000+http.Server.ReadTimeout 和 WriteTimeout,防止慢连接长期占用 goroutine很多团队花大力气写复杂的滑动窗口限流,却忽略了一个事实:80% 的高流量压力来自重复请求、未压缩响应、长连接空耗、JSON 全量解析——这些全在限流之前就能解决。
比如一个 50KB 的 JSON 响应,开启 gzip 后可能只剩 8KB,带宽省了 84%,连接复用率提升后,同样机器能扛住 3 倍 QPS。这种优化不写一行限流代码,效果却远超调参。
gzip(用 gzip.Handler 包裹)json.Decoder 流式解析大请求体,避免 ioutil.ReadAll 把整个 body 读进内存
IdleTimeout(如 30s),及时释放空闲连接,防止连接数虚高context.WithTimeout,防止下游依赖卡死拖垮整条链路真正难的不是写限流算法,而是判断哪一层该限、哪一层该减、哪一层该丢——流量进来时,系统已经没有“完美方案”,只有取舍和优先级。