最稳轻量的IP限流方案是用golang.org/x/time/rate配合可信X-Forwarded-For解析和连接层超时控制;需为每个标准化IP绑定独立rate.Limiter并定时清理,禁用r.RemoteAddr,改用limiter.Wait(ctx)阻塞限流,同时配置ReadHeaderTimeout与MaxConnsPerHost防御慢连接。
直接用 golang.org/x/time/rate 做 ip 级限流,配合可信 x-forwarded-for 解析和连接层超时控制,是当前最稳、最轻量、生产验证过的组合方案。别碰手写计数器、别一上来就上 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实例上:ReadHeaderTimeout: 2 * time.Second——只约束 header 解析阶段,比ReadTimeout更精准,2–3 秒足够正常客户端完成 handshake -
MaxConnsPerHost必须显式设(如 50),尤其用httputil.NewSingleHostReverseProxy时,否则代理层连接无上限 - 搭配内核参数:
net.core.somaxconn ≥ 4096,net.ipv4.tcp_max_syn_backlog同步加大,防止半连接队列溢出 - 务必配合
contextdeadline,否则Wait会永久阻塞;正确写法是传一个带 deadline 的context.WithTimeout(ctx, 3*time.Second)
按 IP + 路径分桶限流才能防住路径级轰炸
单纯在 http.Handler 里套个 rate.NewLimiter(10, 10) 拦不住 CC 攻击——这表示“允许突发 10 次,之后每秒匀速放行 10 次”,攻击者只要每秒发 11 次,桶就持续溢出,限流形同虚设。
立即学习“go语言免费学习笔记(深入)”;
- 限流维度必须是
ip + request.URL.Path,避免单个恶意路径耗尽全局桶 -
burst值应略高于该路径的正常并发峰值(比如 3–5),绝不能设成 100+,否则等于主动放行洪水 -
rps要结合 handler 平均耗时估算:若处理一次请求平均耗时 200ms,单核稳撑上限约 5 rps;设 100 就会引发排队雪崩 - 必须调用
limiter.Wait(ctx),而非Allow()——后者不阻塞,超限请求仍会进入 handler 占用 goroutine
最容易被忽略的点是:limiter.Wait 不带超时就是 goroutine 泄漏温床;而 ReadHeaderTimeout 设太长会让 slowloris 连接卡在连接队列里不动,这两个参数必须配对调整,且要跟你的实际网络 RTT 和上游代理行为对齐。


















