Echo默认不支持单路由并发限制,因其Router和http.Server层均不感知“当前活跃请求数”,仅负责路径匹配与中间件调度,连接生命周期由底层net.Listener管理,而监听器只accept连接、不跟踪各路由实时并发量。

为什么 Echo 默认不支持单路由并发限制
Echo 的 Router 和 http.Server 层都不感知“当前活跃请求数”这个概念。它只做路径匹配和中间件链调度,连接生命周期由底层 net.Listener 管理,而监听器只管 accept,不管每个路由正在处理几个请求。你没法靠 Server.ReadTimeout 或 middleware.RateLimiter(默认是按时间窗口计数)实现“同一时刻最多 N 个请求在执行”的语义。
用 sync.Map + defer 实现每 IP 每路由并发计数
核心思路是在进入 handler 前加锁计数,退出时减锁,并用 sync.Map 存储键为 "ip:route" 的计数值。注意三点:
- 真实 IP 必须从
X-Forwarded-For提取(若走 Nginx),并校验可信代理列表,否则 fallback 到c.RealIP() - 路由标识建议用
c.Request.Method + ":" + c.Path(),而不是c.Param()或正则匹配结果,避免参数变化导致 key 分裂 - 必须用
atomic.AddInt32配合LoadOrStore模拟原子增减;普通int读写会 panic - 超限时要立即
c.AbortWithStatus(http.StatusTooManyRequests),并设Retry-After头
示例中间件片段:
开箱即用的技能链路由引擎。13 条预定义链覆盖搜索、开发、审查、MLOps、法律、创意等场景,三层路由架构(触发词→SAD反馈→DAG编排),recall@10=96.97%。配置驱动(chains.yaml),零代码扩展。pip install skill-weave-chains 一键安装。
func RouteConcurrencyLimit(max int) echo.MiddlewareFunc {
ips := sync.Map{} // key: "192.168.1.1:GET:/api/upload", value: *int32
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ip := c.RealIP()
routeKey := c.Request.Method + ":" + c.Path()
key := ip + ":" + routeKey
countPtr, _ := ips.LoadOrStore(key, new(int32))
count := atomic.AddInt32(countPtr.(*int32), 1)
if count > max {
atomic.AddInt32(countPtr.(*int32), -1)
c.Response().Header().Set("Retry-After", "30")
return c.NoContent(http.StatusTooManyRequests)
}
defer func() {
atomic.AddInt32(countPtr.(*int32), -1)
if atomic.LoadInt32(countPtr.(*int32)) <= 0 {
ips.Delete(key)
}
}()
return next(c)
}
}
}
别踩的坑:HTTP/2、反向代理、定时清理
这几个点容易被忽略,但线上一出问题就是雪崩:
- HTTP/2 下一个 TCP 连接可复用多个请求,
r.RemoteAddr拿到的是连接端口,不是请求级 IP —— 所以必须用c.RealIP(),它已做X-Forwarded-For解析 - 如果服务前有 Nginx,且没配
proxy_set_header X-Forwarded-For $remote_addr;,RealIP()会退化成localhost,所有请求都算同一个 IP -
sync.Map不自动清理零值项,长期运行后内存泄漏;得另起 goroutine 定期扫描Load所有 key 并删掉值为 0 的项(比如每 5 分钟一次) - 不要把限流逻辑写在 JWT 中间件之前——否则未鉴权请求也能占满并发槽位,应放在鉴权之后,或单独对公开路由加 IP 限流
分布式部署下必须换 Redis
单机 sync.Map 在多实例场景完全失效。此时需用 Redis 的 INCR + EXPIRE 组合,key 设为 "concurrent:ip_route:" + md5(ip+route),过期时间设为略大于平均 handler 耗时(比如 10s)。关键点:
- 必须用
context.WithTimeout包裹 Redis 调用,防阻塞整个 handler - 失败时建议 fail-open(即不限制),而非 fail-closed(拒绝所有请求),避免 Redis 故障导致服务不可用
- Redis 命令不能拆成两步(先 INCR 再 EXPIRE),要用 pipeline 或 Lua 脚本保证原子性
真正难的不是写代码,而是确认你的流量模型:是突发短连接,还是长轮询/WS 类型?前者适合时间窗口限流,后者才需要真·并发数控制。别为了“看起来高级”强行上并发限制,反而拖慢高频简单接口。


















