fiber 的 limiter.new() 不适用,因其基于时间窗口统计请求数,无法限制并发连接数;需用 semaphore 按 ip 实现请求级并发控制,并协同配置连接池、超时与清理机制。

不能直接用 Fiber 内置中间件限制单个 IP 的并发连接数,必须结合连接池管理 + 请求级限流器(如 Semaphore)或舱壁(Bulkhead)实现。
为什么 Fiber 的 limiter.New() 中间件不适用
Fiber 自带的 limiter.New() 是基于时间窗口的速率限制(如 “每分钟最多 100 次请求”),它统计的是请求数,不是并发连接数。即使你设成 max: 1,也只是限制“每秒/每分钟最多 1 次”,无法阻止第 2 个请求在第 1 个尚未返回时就进入处理流程——这正是并发连接爆炸的根源。
- 它不感知 TCP 连接生命周期,也不控制 Goroutine 并发数
- 对长耗时后端调用(如 HTTP 调用、数据库查询)完全无效
- 无法防止恶意客户端持续发起阻塞型请求占满 worker 线程
真正有效的方案:用 Semaphore 在 handler 入口做并发许可
核心思路是:每个 IP 对应一个独立的信号量,每次请求进来先 acquire(),处理完再 release()。信号量容量即为该 IP 最大允许的并发请求数。
- 推荐用
golang.org/x/sync/semaphore,轻量、无锁、支持上下文取消 - 用
sync.Map缓存每个 IP 的*semaphore.Weighted实例,避免频繁新建 - 务必设置合理的
context.WithTimeout,防止 acquire 长期阻塞
var ipSemaphores sync.Map // map[string]*semaphore.Weighted
func concurrencyLimiter(maxPerIP int64) fiber.Handler {
return func(c *fiber.Ctx) error {
ip := c.IP()
sem, _ := ipSemaphores.LoadOrStore(ip, semaphore.NewWeighted(maxPerIP))
if !sem.(*semaphore.Weighted).TryAcquire(1) {
return c.Status(fiber.StatusTooManyRequests).SendString("concurrent limit exceeded")
}
defer sem.(*semaphore.Weighted).Release(1)
return c.Next()
}
}
注意连接池和下游依赖的协同配置
仅限流入口还不够。如果 handler 内部调用第三方 API,而你用的是 http.Client,它的默认连接池(DefaultTransport)没有按 IP 隔离,仍可能被单个恶意 IP 触发大量出站连接。
- 对下游 HTTP 调用,需为每个 client 分配独立的
http.Transport,并设MaxIdleConnsPerHost = 5(与你的maxPerIP匹配) - 若使用
resty或自定义http.Client,确保Timeout、IdleConnTimeout、MaxIdleConnsPerHost均显式配置,否则限流形同虚设 - 避免在 handler 中启动无限制 goroutine(如
go doSomething()),那会绕过Semaphore控制
生产环境必须考虑的清理与降级
sync.Map 中的 semaphore 实例不会自动过期,长期运行会导致内存泄漏(尤其当 IP 频繁变化时)。
- 建议加定时清理:每 5 分钟遍历一次
ipSemaphores,删除超过 10 分钟未访问的条目 - 全局 fallback:设置总并发上限(如用一个全局
Semaphore),防止极端情况下 IP 数暴增拖垮服务 - 日志埋点:记录被拒绝的 IP 和时间,用于后续分析是否需加入黑名单
真正难的不是写几行限流代码,而是让信号量、HTTP 连接池、超时链路、错误恢复全部对齐。漏掉任意一环,限流就只是个心理安慰。











