直接用 net/http 的 remoteaddr 做限流会出错,因为其返回的是连接来源(如 nginx、cloudflare 或 kubernetes ingress 的地址),而非真实客户端 ip;在代理后场景下所有请求可能被识别为同一 ip,导致限流失效或误杀。

为什么直接用 net/http 的 RemoteAddr 做限流会出错
Gin 中的 c.ClientIP() 才是正确获取客户端真实 IP 的方式,c.Request.RemoteAddr 返回的是连接来源(可能是代理或负载均衡器的地址),在 Nginx、Cloudflare 或 Kubernetes Ingress 后面时基本不可信。如果直接拿 RemoteAddr 做 key 限流,会导致所有用户被当成同一个 IP 处理,或者漏掉真实用户。
- 务必启用
GIN_MODE=release并配置c.SetTrustedProxies(),否则c.ClientIP()可能返回空或错误值 - 若反向代理未设置
X-Forwarded-For或X-Real-IP头,c.ClientIP()会 fallback 到RemoteAddr,仍不可靠 - 测试时用
curl -H "X-Forwarded-For: 192.168.1.100"模拟真实链路,别只跑本地localhost
用 golang.org/x/time/rate 实现每 IP 独立令牌桶
不要全局共用一个 rate.Limiter,必须按 IP 动态创建并缓存,否则所有请求竞争同一桶,失去“按 IP 限流”意义。但也不能无限制新建 goroutine 或 map entry,需加清理机制。
- 用
sync.Map存储map[string]*rate.Limiter,避免并发写 panic - 为每个 IP 设置独立的
rate.NewLimiter(10, 20):每秒最多 10 次,初始容量 20(允许突发) - 定期清理长时间未访问的 IP 条目(例如 30 分钟无请求),防止内存泄漏;可用
time.AfterFunc或后台 goroutine 扫描 - 注意
rate.Limiter.AllowN(time.Now(), n)返回 false 时应立即c.AbortWithStatusJSON(429, ...),不要继续后续 handler
Gin 中间件里怎么安全提取和校验 IP
中间件必须在路由匹配前执行,且 IP 提取逻辑要包裹在 defer 或 recover 中——某些异常请求(如畸形 header)可能让 c.ClientIP() panic。
- 调用
c.ClientIP()前先确认c.Request不为 nil,再检查返回值是否为空字符串或"::1"(本地 IPv6) - 对 IP 做标准化:IPv6 地址用
net.ParseIP(ip).To16()统一为 16 字节格式,避免同一 IP 多种表示(如::1和0:0:0:0:0:0:0:1) - 敏感服务建议额外过滤私有网段(
10.0.0.0/8、172.16.0.0/12、192.168.0.0/16),防止内网穿透绕过限流 - 不要在中间件里做耗时操作(如查数据库验证 IP 白名单),否则拖慢所有请求;白名单逻辑应预加载到内存 map
生产环境必须加 Redis 支持分布式限流
单机内存限流在多实例部署下完全失效。Gin 本身不提供跨进程状态同步,必须依赖外部存储。Redis 的 INCR + EXPIRE 原子组合是最轻量可靠的方案,比 Redlock 或 Lua 脚本更易维护。
- Key 设计为
rate_limit:ip:<code>hash(ip):20240520:14(按天+小时分片,避免单 key 过热) - 用
redis.Client.Incr(ctx, key)+redis.Client.Expire(ctx, key, 1*time.Hour),注意两次调用非原子,需用 pipeline 或 Lua 保证一致性 - 失败时降级为本地内存限流(仅限临时应急),不能直接放行;记录日志并告警 Redis 不可用
- QPS 高时,考虑用 Redis Cluster 分片 key,避免单节点瓶颈;key 的 hash 取模应避开常见 IP 段分布偏差
IP 限流真正的难点不在代码实现,而在识别真实客户端、应对代理链路、以及分布式场景下 Redis 的延迟与失败处理——这些地方出问题,监控指标往往滞后,等发现时流量早已打穿后端。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











