全局限流和单ip限流必须分开配置、分层生效,因二者目标不同:全局限流保护服务整体容量,单ip限流抑制异常行为;混用echo默认ratelimiter中间件会导致所有请求共享同一计数器,引发策略冲突或失效。

全局限流和单IP限流必须分开配置、分层生效,混用 echo.RateLimiter 的默认中间件会导致策略冲突或完全失效。
为什么不能只用一个限流中间件
Echo 的 middleware.RateLimiter 默认基于内存计数器(middleware.NewRateLimiter),它不区分请求来源,所有请求共享同一计数器。这意味着:如果设了每秒 100 次全局限流,而某个恶意 IP 发起 99 次请求,其余所有用户只剩 1 次配额——根本起不到防护作用。
反过来,若只配单 IP 限流(如每秒 5 次),攻击者用 20 个 IP 就能轻松绕过,总 QPS 瞬间飙到 100+,压垮后端服务。
- 全局限流目标是保护服务整体容量(CPU/DB连接池/上游API配额)
- 单IP限流目标是识别并抑制异常行为(爬虫、爆破、脚本调用)
- 两者触发条件、统计维度、恢复机制完全不同,必须独立部署
用 middleware.NewRateLimiter 实现全局限流
全局限流需全局唯一计数器,推荐使用 rate.Limiter(来自 golang.org/x/time/rate)配合内存锁或 Redis。纯内存方案适合单实例,注意避免 goroutine 泄漏:
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
var globalLimiter = rate.NewLimiter(rate.Every(time.Second), 100) // 100 QPS
e.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
Skipper: middleware.DefaultSkipper,
// 注意:这里不能用 middleware.NewRateLimiter(),它内部是 per-request 计数
// 必须自己实现 GetKey 和 Limit
LimitFunc: func(c echo.Context) (int, time.Duration, error) {
return 100, time.Second, nil // 固定返回全局配额
},
KeyFunc: func(c echo.Context) string {
return "global" // 所有请求都走同一个 key
},
}))
- 不要依赖
middleware.NewRateLimiter的默认行为,它按c.Request().RemoteAddr分 key,不是全局的 - 若部署多实例,必须换为 Redis 后端(如
github.com/ulule/limiter+redisstore) - 配额值建议留 20% 余量,防止突发流量打满后无法响应健康检查
用 middleware.RateLimiter 配 IP 维度限流
这才是 middleware.RateLimiter 的典型用法:按客户端真实 IP 做隔离计数。关键在 KeyFunc 提取 IP,且要处理代理场景:
e.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
KeyFunc: func(c echo.Context) string {
ip := c.RealIP() // 自动解析 X-Forwarded-For / X-Real-IP
if ip == "" {
ip = c.Request().RemoteAddr
}
return ip
},
LimitFunc: func(c echo.Context) (int, time.Duration, error) {
return 5, time.Second, nil // 单 IP 每秒最多 5 次
},
}))
-
c.RealIP()是安全起点,但前提是 Nginx 或其他反向代理已正确设置X-Forwarded-For,否则会拿到内网地址 - 别用
c.Request().RemoteAddr直接当 key,它带端口(如192.168.1.100:54321),导致同一 IP 多次请求被算作不同 client - 时间窗口建议用
time.Minute替代time.Second,防短时毛刺误封正常用户
两层限流的执行顺序与 fallback 逻辑
限流中间件的注册顺序决定拦截优先级。应让单 IP 限流在前,全局在后:
e.Use(ipBasedRateLimiter) // 先拦住恶意 IP e.Use(globalRateLimiter) // 再保底控总量
这样设计的好处是:单 IP 触发限流时,错误响应(429 Too Many Requests)更早返回,不消耗全局配额;而全局限流作为兜底,只在大量 IP 同时高频请求时生效。
- 避免把全局限流放前面——它会让所有请求先排队等令牌,增加延迟,还掩盖了真实攻击源
- 两个中间件的
LimitFunc返回的错误必须显式处理,否则 Echo 默认返回空 429 响应,前端无法区分是 IP 被封还是服务过载 - 生产环境务必开启
middleware.RateLimiterConfig.OnLimitReached回调,记录被限流的 IP 和路径,用于后续分析
真正难的是动态调整阈值和跨节点状态同步。硬编码的数字在流量突增或灰度发布时极易失效,而 Redis 限流又引入额外延迟和故障点——这些细节往往比“怎么写”更决定系统是否扛得住。










