rate.Limiter不支持IP段限流,因其仅接受精确key(如单IP字符串),无法自动匹配CIDR网段;需先解析客户端IP,再用预解析的*net.IPNet调用Contains判断归属,最后按优先级映射到对应限流器。

为什么不能直接用 rate.Limiter 做 IP 段限流
IP 段(比如 192.168.0.0/16)不是单个 IP,而是一组地址的集合。但 golang.org/x/time/rate.Limiter 本身不支持“范围匹配”,它只认具体 key(如字符串 "192.168.0.1")。如果你把整个 CIDR 当作 key 去查 sync.Map,那 "192.168.0.0/16" 和 "192.168.1.100" 就是两个完全无关的 key,限流逻辑就断了。
必须自己做 IP 归属判断 —— 即每次请求进来,先解析客户端真实 IP,再检查它是否落在某个预设网段内,再映射到对应的限流器实例。
- 别依赖
c.ClientIP()原始值:它可能被伪造,得先调router.SetTrustedProxies并信任X-Forwarded-For - 别在中间件里硬编码
net.ParseCIDR("192.168.0.0/16")每次都解析:提前 parse 好存成*net.IPNet,运行时只调ipNet.Contains(clientIP) - 多个网段之间不能有重叠,否则一个 IP 匹配多个规则,该走哪个限流器?得定义优先级(比如按掩码长度从长到短匹配)
如何实现 CIDR 到限流器的动态映射
核心是构建一张「网段 → *rate.Limiter」的查找表,并在请求时快速命中。推荐用切片 + 线性扫描,而不是 map —— 因为网段数量通常很小(
示例结构:
type CIDRRateRule struct {
Network *net.IPNet
Limiter *rate.Limiter
}
var cidrRules = []CIDRRateRule{
{Network: mustParseCIDR("10.0.0.0/8"), Limiter: rate.NewLimiter(rate.Limit(1), 1)},
{Network: mustParseCIDR("172.16.0.0/12"), Limiter: rate.NewLimiter(rate.Limit(5), 5)},
{Network: mustParseCIDR("192.168.0.0/16"), Limiter: rate.NewLimiter(rate.Limit(10), 10)},
}
-
mustParseCIDR是封装好的 panic-safe 解析函数,只在 init 阶段调用一次 - 匹配顺序很重要:把更精确的网段(如
/24)放在前面,避免被宽泛网段(如/8)提前截获 - 没匹配上的请求,默认走 fallback limiter(比如全局限流或放行),不要 panic 或 500
中间件里怎么安全地做 IP 提取和限流判断
关键点不是“能不能限”,而是“限得准不准、会不会崩”。真实部署中,以下三件事漏掉任一,都会导致限流失效或服务抖动:
- 必须在限流中间件之前注册认证/可信代理中间件,否则
c.ClientIP()返回的是 LB 地址(如10.10.10.10),而非真实用户 IP - 提取出的 IP 必须转成
net.IP(用net.ParseIP),不能直接拿字符串去比对 —— IPv4 和 IPv6 的格式差异会导致Contains失败 - 不要在每次请求里 new
rate.Limiter:每个网段复用一个实例;但要注意rate.Limiter是并发安全的,无需额外加锁
典型中间件片段:
func CIDRRateLimitMiddleware(rules []CIDRRateRule, fallback *rate.Limiter) gin.HandlerFunc {
return func(c *gin.Context) {
clientIP := net.ParseIP(c.ClientIP())
if clientIP == nil {
// 解析失败,走 fallback
if !fallback.Allow() {
c.Header("X-RateLimit-Remaining", "0")
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "too many requests"})
return
}
c.Next()
return
}
var limiter *rate.Limiter
for _, rule := range rules {
if rule.Network.Contains(clientIP) {
limiter = rule.Limiter
break
}
}
if limiter == nil {
limiter = fallback
}
if !limiter.Allow() {
c.Header("X-RateLimit-Remaining", "0")
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "too many requests"})
return
}
c.Next()
}
}
多实例部署时,这个方案还管用吗
不管用。上面所有基于内存的 sync.Map 或切片匹配,只在单进程内有效。一旦上了负载均衡,同一 IP 的请求可能打到不同机器,每台机器各自计数,总 QPS 就会翻倍甚至失控。
真要跨实例做 IP 段限流,唯一可靠路径是 Redis + Lua:
- Redis key 设计为
rate:cidr:<code>matched_network_string:client_ip(例如rate:cidr:192.168.0.0%2F16:192.168.5.20) - Lua 脚本里做两件事:1)根据 client IP 反查它属于哪个 CIDR(需提前把 CIDR 规则 dump 到 Redis 的 hash 或 sorted set 中);2)对该 CIDR 下的该 IP 执行原子计数 + 过期
- 别省事用
INCR+EXPIRE分两步:竞态下可能 key 过期了但 INCR 还成功,导致漏限
也就是说:单机可用靠切片 + net.IPNet.Contains;生产环境必须上 Redis,且 Lua 脚本里得重现实现 CIDR 匹配逻辑 —— 这部分没法交给 Redis 做,得你自己写。


















