kratos的ratelimit.server工作在http请求处理阶段,仅统计单位时间请求数,无法感知tcp连接状态,故不适用于ip并发连接限制;正确做法是包装net.listener层,在accept时提取真实ip并计数准入。

Kratos 没有内置按 IP 限并发连接数的能力,必须自己实现中间件或下沉到 net.Listener 层;直接套用 ratelimit.Server 或 bbr 限流器只能控请求频次,对长连接、HTTP/2 多路复用完全无效。
为什么 Kratos 的 ratelimit 中间件不适用于 IP 并发连接限制
Kratos 的 ratelimit.Server(无论用 bbr 还是 tokenbucket)工作在 HTTP 请求处理阶段,它统计的是「单位时间内的请求数」,而非「当前活跃 TCP 连接数」:
- 一个浏览器标签页开启 HTTP/1.1 keep-alive 后,可能复用同一连接发几十个请求,但只算 1 个连接 → ratelimit 会放过它
- WebSocket 或 SSE 长连接持续占用 socket,但几乎不发新请求 → ratelimit 完全不感知
- HTTP/2 下单个连接可承载上百 stream,
limit_conn perip 5在 Nginx 层已失效,而 Kratos 更无从得知底层连接状态 - 限流中间件在
http.Handler执行时才介入,此时连接早已建立、资源已被分配
正确做法:在 net.Listener 层拦截并计数
和 Go 标准库一样,Kratos 的 http.Server 底层仍依赖 net.Listener。要真正限制「每个 IP 当前有多少个 TCP 连接处于 ESTABLISHED 状态」,必须包装 Listener,在 Accept() 时提取 IP 并做准入判断:
- 用
sync.Map或带读写锁的map[string]int存储IP → 连接数,key 提取需规范:IPv4 直接取点分十进制(如"192.168.1.100"),IPv6 去掉方括号并标准化(如"2001:db8::1") - 在
Accept()返回前检查计数,超限时调用conn.Close()并返回nil, errors.New("too many connections")——http.Server会忽略该 error 并继续 accept - 必须实现
Close()和Addr()方法,否则 Kratos 启动报错 - 不要依赖
X-Forwarded-For:它在 Listener 层尚未解析,且可伪造;真实 IP 只能来自conn.RemoteAddr().String()(若服务直面用户)或由前置代理(如 Nginx)通过PROXY protocol透传
更稳妥的组合方案:iptables + Kratos 中间件协同
纯应用层控制有延迟和绕过风险。生产环境应分层防御:
- 第一道防线:
iptables -A INPUT -p tcp --dport 8000 -m connlimit --connlimit-above 10 --connlimit-mask 32 -j REJECT --reject-with tcp-reset,在内核层实时拦截新连接,不消耗 Kratos 任何 CPU 或 goroutine - 第二道防线:Kratos 自定义中间件(非 Listener 层),仅用于业务逻辑级防护 —— 例如对
/api/login路径,在 handler 内检查realIP(r)的当前活跃请求数(用sync.Map+atomic),超限返回http.StatusTooManyRequests - 注意区分场景:Listener 层限制的是「TCP 连接数」,中间件限制的是「HTTP 请求并发数」;前者防资源耗尽,后者防后端处理过载
最容易被忽略的一点:Kratos 启动时默认使用 http.DefaultServeMux,但它的 Server 实例并不暴露 Listener 控制权。你必须手动构造 http.Server,传入自定义 Listener,再用 srv.Serve(lis) 启动 —— 直接调 kratos.Run() 无法插入 Listener 层逻辑。











