Go标准库不主动探活空闲连接,仅靠IdleConnTimeout被动淘汰;HTTP客户端可启用TCPKeepAlive,数据库驱动(如mysql)在取连接前非阻塞读字节检测,Redis默认配合健康检查。

空闲连接怎么检测是否还活着
Go 标准库本身不主动对空闲连接做探活(ping),只靠 IdleConnTimeout 被动淘汰。这意味着:连接可能在服务端已断开(比如防火墙 5 分钟 kill 空闲 TCP 连接),但客户端池里还留着,下次复用就报 i/o timeout 或 connection refused。
解决办法分两层:
- HTTP 客户端:启用
TCPKeepAlive,让 OS 层定期发心跳包,提前暴露断连(注意它不保证应用层可用,但能减少“假连接”) - 数据库(如
go-sql-driver/mysql):依赖驱动内置的连接检查逻辑——每次从池里取出连接前,会尝试非阻塞读一个字节;若返回io.EOF或系统错误,就丢弃该连接并新建一个 - Redis(
go-redis):默认开启PoolSize+MinIdleConns配合健康检查,但需确保ReadTimeout和WriteTimeout合理,否则探活超时反拖慢请求
SetConnMaxLifetime 为什么不能设为 0
SetConnMaxLifetime(0) 表示连接永不过期,这是危险操作。实际运行中,中间网络设备(NAT、LB)、云厂商 SLB、甚至 MySQL 自身的 wait_timeout 都会单方面关闭长连接,而 Go 的连接池并不知情。
后果是:连接在池里“僵尸化”,后续复用直接失败。正确做法是设一个略小于服务端超时值的生命周期,例如:
立即学习“go语言免费学习笔记(深入)”;
- MySQL 默认
wait_timeout = 28800(8 小时),建议SetConnMaxLifetime(7 * time.Hour) - PostgreSQL 常配
tcp_keepalives_idle = 60,对应池内连接寿命设为50 * time.Minute - Redis 无原生连接超时,但建议统一设为
30 * time.Minute防止累积 stale 连接
连接池耗尽却没满:WaitCount 持续上涨的真因
现象:db.Stats().InUse 远低于 MaxOpenConns,但 WaitCount 不断上升,接口延迟飙升。这不是连接不够,而是连接被“卡住”了。
根本原因通常是没用 Context 控制操作生命周期:
- 调用
db.Query()而非db.QueryContext(ctx, ...),遇到慢查询或网络抖动时,连接一直被占着不归还 - HTTP 客户端未设置
Timeout或未调用resp.Body.Close(),导致底层 TCP 连接无法释放回池 - 自定义连接池(如 Redis、gRPC)里,
Get()后忘记Close()或 panic 未 recover,连接泄露
验证方式:持续观察 WaitDuration 是否线性增长,配合 pprof 查 goroutine 堆栈看哪些请求卡在 net.Conn.Read 或 mysql.(*mysqlConn).writePacket。
go-redis 和 http.Client 的重试策略差异
两者都支持重试,但语义完全不同,混用会出问题:
-
http.Client的重试必须由上层业务控制——因为 HTTP 请求不幂等(如 POST 提交订单),标准库不自动重试,MaxRetryBackoff只在 Transport 层用于连接建立阶段 -
go-redis的MaxRetries是客户端内置的,对大多数命令(GET/SET/INCR)默认启用,且重试前会先标记连接为不可用,再从池里取新连接;但对事务(MULTI/EXEC)或 Lua 脚本,重试可能破坏一致性,需手动禁用 - 关键区别:
http.Transport重试只发生在 dial 阶段;go-redis重试发生在命令执行失败后,且会切换连接,更贴近“故障转移”而非“重试”
真正容易被忽略的是:当使用 redis.ClusterClient 时,重试逻辑还会结合 slot 迁移和 MOVED/ASK 重定向,此时 MinRetryBackoff 设太小会导致大量无效重定向请求打到错误节点。


















