用 ss -s 和 db.Stats() 可快速确认连接池是否耗尽:若 InUse ≈ MaxOpenConns、Idle 长期为 0、WaitCount 持续上涨,且 ss -s 中 TIME_WAIT 超 2 万,则大概率因连接未复用导致端口耗尽或池卡死。

用 ss -s 和 db.Stats() 快速确认连接池是否真耗尽
别等线上崩了才查。本地压测时,先看两个最直接的指标:ss -s 显示系统级连接状态,db.Stats() 或 http.DefaultTransport.IdleConnTimeout 暴露应用层连接池水位。
常见误判是把 context deadline exceeded 当成下游慢,其实可能是连接池卡死。只要出现这三点同时成立,基本就是耗尽: InUse 接近或等于 MaxOpenConns、Idle 长期为 0、WaitCount 持续上涨。
-
ss -s里TIME-WAIT超过 2 万,大概率是 HTTP 连接没复用,端口在耗尽边缘 - 对 MySQL,
SHOW PROCESSLIST里活跃连接数远低于MaxOpenConns,但应用侧WaitCount一直在涨 → 连接卡在“等释放”,不是 SQL 慢 - HTTP 场景下,
curl -v http://your-service/health返回超时,但netstat -an | grep :8080 | wc -l显示 ESTABLISHED 很少 → 问题不在监听队列,而在出站连接池
并发请求时故意触发 WaitCount 上升
写一个最小可复现脚本,不依赖外部服务,只靠本地连接池行为就能暴露问题。核心是控制并发数略高于连接池上限,让 goroutine 主动排队。
比如数据库池设 SetMaxOpenConns(5),然后起 8 个 goroutine 同时执行 db.QueryRow("SELECT 1"),并在每个 query 前加 time.Sleep(100 * time.Millisecond) 模拟处理延迟。这样前 5 个会拿到连接,后 3 个必然阻塞在 WaitCount++,几秒内就能从 db.Stats() 看到 WaitCount 和 WaitDuration 明显增长。
- HTTP 同理:用
&http.Transport{MaxIdleConnsPerHost: 2},并发发 10 个请求到同一 host,http.DefaultClient就会频繁新建连接,ss -s中TIME-WAIT数量会指数上升 - 别用
http.Get测试——它用的是DefaultClient,你改不了 Transport;必须手造&http.Client{Transport: yourTransport} - 加一行日志:
log.Printf("pool stats: %+v", db.Stats()),每秒打一次,比看 pprof 更快定位拐点
为什么 pprof heap 看不到连接泄漏,但连接数还在涨
因为泄漏的不是 Go 对象,而是操作系统资源:文件描述符(fd)和 TCP 连接。pprof heap 只统计堆上分配的 *sql.conn 或 *http.persistConn,但这些结构体可能已被 GC,而底层 fd 还卡在 CLOSE_WAIT 或 TIME_WAIT 状态没释放。
- 查 fd 数量:
lsof -p $(pidof your-binary) | wc -l,超过 65535 就危险了 - 查具体连接状态:
ss -tan state time-wait | wc -l,超过 5000 就说明复用失败 - HTTP 场景下,
httptrace能抓到每次请求是否命中空闲连接:GotConn字段为 false 表示新建连接,连续多次 false 就是复用失效 - 数据库场景下,
db.SetConnMaxLifetime(240 * time.Second)必须设,否则连接被 MySQL 主动断开后,Go 客户端还当它是活的,后续 query 直接报driver.ErrBadConn
事务没 Commit/Rollback 是最隐蔽的耗尽源头
它不会立刻报错,也不会进 WaitCount,而是把连接死锁在 *sql.Tx 里,InUse 持续占着不放,Idle 永远为 0。压测时你看到 InUse == MaxOpenConns 且 WaitCount == 0,反而更危险——说明连接全被事务绑死了。
- 典型漏点:
if err != nil { return }前没tx.Rollback();defer tx.Commit()被 panic 绕过;HTTP 调用超时后忘了 rollback - 安全写法:用闭包 defer 检查状态:
defer func() { if r := recover(); r != nil { tx.Rollback() } else if err == nil { tx.Commit() } else { tx.Rollback() } }() - GORM 用户注意:
db.Transaction()回调里 return error 会自动 rollback,但手动开db.Begin()就必须自己管到底
真正难排查的不是参数设多大,而是哪些地方根本没走连接归还路径。比如 rows.Close() 忘了调、resp.Body.Close() 在 error 分支外、或者用了 io.Copy(ioutil.Discard, resp.Body) 却没关 body ——这些都会让连接卡住,直到 IdleConnTimeout 到期才释放,期间完全不可复用。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











