gin.default()会拖垮长连接场景,因其默认启用同步i/o的logger和recovery中间件,且net/http.server超时参数全为0,导致空闲连接不释放、文件描述符耗尽;应改用gin.new()并配置idletimeout等关键超时。

Gin 本身不维持连接,它只是处理 HTTP 请求的框架;真正决定能否撑住海量长连接(比如 100 万)的是 net/http.Server 的配置、操作系统参数、Go 运行时设置,以及你是否用对了通信模式。
为什么 gin.Default() 会拖垮长连接场景
默认引擎自带 gin.Logger() 和 gin.Recovery() 中间件,它们在每次请求中都做同步 I/O(如写文件、刷日志缓冲区),尤其在高并发下会成为锁竞争热点。更关键的是:gin.Default() 没有设置任何超时,net/http.Server 的 ReadTimeout、WriteTimeout、IdleTimeout 全部为 0 —— 这意味着空闲连接永不关闭,文件描述符持续累积,直到系统 ulimit -n 耗尽。
- 必须用
gin.New()替代gin.Default(),手动注册必要中间件 - 所有日志输出应异步(例如写入 channel + 单独 goroutine 刷盘),或直接对接
zerolog/zap的异步 writer - 禁用默认恢复中间件,改用带 context 超时控制的 panic 捕获逻辑
net/http.Server 层的关键 timeout 配置
连接数爆炸的主因不是 Gin,而是 http.Server 没设 IdleTimeout。HTTP/1.1 keep-alive 下,客户端不主动断连,服务端若不强制清理,连接就一直挂着。以下配置是底线:
srv := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second, // ⚠️ 必须设!否则连接永不释放
MaxHeaderBytes: 1
-
IdleTimeout是长连接的生命线,建议 15–45s,具体看客户端心跳间隔 -
ReadTimeout应略大于最长业务处理时间,避免半开连接堆积 - 不要设
ReadHeaderTimeout过短(如 -
http.Server不支持 per-connection 自定义超时,所有连接共享同一套 timeout
操作系统与 Go 运行时联动调优
即使代码全优化,Linux 默认的 ulimit -n(通常 1024)和 net.core.somaxconn(通常 128)也会在几万连接时就卡死。这不是 Gin 的锅,但你得亲手调。
- 启动前执行
ulimit -n 1048576,并在 systemd service 文件里加LimitNOFILE=1048576 -
sysctl -w net.core.somaxconn=65535,并写入/etc/sysctl.conf - Go 程序启动时显式设
runtime.GOMAXPROCS(runtime.NumCPU()),避免调度器被压垮 - 禁用 GC 频繁触发:通过
debug.SetGCPercent(20)降低回收频率(需配合内存监控) - 注意:Gin 的
sync.Pool复用*gin.Context有效,但无法缓解 goroutine 堆积——每个连接一个 goroutine 是 net/http 的行为,不是 Gin 能改的
真正该换技术栈的信号
如果你的需求本质是「维持百万级双向长连接」(比如教练在线状态、实时预约推送),Gin + HTTP/1.1 就是错选。它的设计目标是 request-response,不是 connection lifecycle management。
- HTTP/2 多路复用能缓解部分压力,但 Gin 对 HTTP/2 的支持仍依赖
net/http,且无法解决连接保活逻辑复杂的问题 - WebSocket 场景下,应直接基于
gorilla/websocket或gobwas/ws构建,把 Gin 仅用作登录、鉴权等短连接接口 - 若必须用 Gin 承载长连接语义,务必剥离所有中间件中的阻塞操作,并将连接状态外置到 Redis Cluster(用
SET key value EX 30 NX做心跳续期)
最常被忽略的一点:连接数 ≠ 并发请求数。100 万在线连接,可能每秒只有几百个真实消息,这时候瓶颈从来不在 Gin,而在你有没有把心跳检测、消息广播、连接剔除这些逻辑做成非阻塞 + 批处理。











