直接用golang.org/x/time/rate做ip级限流并结合连接层超时控制,是当前最稳、最轻量、生产验证过的方案;需标准化真实ip(非r.remoteaddr)、用sync.map绑定独立limiter并定时清理、改用limiter.wait(ctx)阻塞限流,同时配置read/write/idletimeout及connstate回调防御慢连接。
直接用 golang.org/x/time/rate 做 ip 级限流 + 连接层超时控制,是当前最稳、最轻量、生产验证过的组合方案。别碰手写计数器、别一上来就上 redis——90% 的场景单机 rate.limiter 就够用,关键是怎么用对。
为什么不能直接用 r.RemoteAddr 当作真实 IP
r.RemoteAddr 在有 Nginx、Cloudflare 或 K8s Ingress 的场景下,几乎总是反向代理或负载均衡器的内网地址,不是用户真实出口 IP。硬编码取它会导致全站限流失效或误杀 NAT 后大量用户。
- 真正可信任的只有你自己的反向代理加的 header,而且必须满足两个条件:Nginx 配了
set_real_ip_from+real_ip_header X-Real-IP - Go 层要做的是:只从
X-Forwarded-For头里取最后一个非内网地址(用net.ParseIP+net.Contains判断是否属于"10.0.0.0/8"、"172.16.0.0/12"、"192.168.0.0/16") - 忽略所有来自非可信网段的
X-Forwarded-For,防止伪造 - fallback 到
r.RemoteAddr时,先用strings.Split(r.RemoteAddr, ":")[0]提取 IP,再用net.ParseIP格式化(兼容 IPv6 方括号)
怎么给每个 IP 绑定独立的 rate.Limiter
全局只建一个 rate.Limiter 是最大误区,所有 IP 共享额度等于没限;但为每个新 IP 都 new 一个又不清理,sync.Map 会持续膨胀。
- 用
sync.Map存储map[string]*rate.Limiter,key 是标准化后的客户端 IP(不是r.RemoteAddr) - 每个 IP 对应一个
rate.NewLimiter(rate.Every(200*time.Millisecond), 3):表示平均 200ms 放一个请求,允许最多积压 3 次突发 - 设置定时器(比如每分钟)遍历
sync.Map,对 30 分钟内无访问的 IP 调用Delete清理 - 避免在 handler 内部调用
limiter.Allow()——它不阻塞,高并发下可能瞬间打穿阈值;改用limiter.Wait(ctx),传入带 deadline 的context
连接层防御比应用层限流更早生效
CC 攻击不全是“高频请求”,还有 slowloris 类型的慢速连接轰炸:建连后只发部分 header,长期不发 body,吃光 http.Server.MaxConns。这时候路由中间件还没执行,限流已经晚了。
- 必须在
http.Server初始化时设置ReadTimeout、WriteTimeout和IdleTimeout,例如IdleTimeout: 30 * time.Second - 启用
http.Server.ConnState回调,对http.StateNew和http.StateActive做连接数粗粒度压制(如限制单 IP 并发连接数 ≤5) - 若用标准库
net/http,无法精细控制连接建立阶段;可考虑用golang.org/x/net/http2配合自定义tls.Config.GetConfigForClient做 TLS 层连接拦截(适用于 HTTPS 入口)
IP 标准化、sync.Map 生命周期管理、连接层 timeout 配置这三处最容易被跳过,但任一缺失都会让整个防护链路失效。尤其是 fallback 到 r.RemoteAddr 时忘记做 IPv6 方括号清理,会导致大量合法 IPv6 请求被误判为无效 IP 而丢弃。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











