连接复用失败的典型现象是大量TIME_WAIT或新ESTABLISHED连接、request canceled超时、IdleConns长期为0;根本原因包括未调resp.Body.Close()、IdleConnTimeout设置不当、TCPKeepAlive与HTTP层timeout混淆,以及服务端主动断连导致复用链路中断。
连接复用失败的典型现象
明明开了 keep-alive,但 netstat -an | grep :443 里始终看到大量 time_wait 或新建立的 established 连接;压测时出现大量 net/http: request canceled (client.timeout exceeded while awaiting headers);监控发现 http.transport.idleconnmetrics().idleconns 长期为 0 或波动剧烈。这些都不是“没开 keep-alive”,而是复用链路某处断了。
http.Transport 的探活参数怎么配才不白配
OS 层 TCP keepalive 和 HTTP 层 idle timeout 是两回事,不能互相替代:
-
TCPKeepAlive:设为30 * time.Second,启用内核级心跳,只探测链路是否通(比如中间防火墙静默丢包),不保证服务端仍接受该连接 -
IdleConnTimeout:必须比服务端的keepalive_timeout(如 Nginx 默认 75s)短,建议60 * time.Second;设太短(如 10s)会让连接刚空闲就回收,复用率归零 -
ResponseHeaderTimeout和DialTimeout要明确设置,否则默认 0(无限等待),协程卡死不释放连接
注意:TCPKeepAlive 在云环境(如 AWS ALB、阿里云 SLB)中作用有限,因 LB 常主动中断长空闲连接,此时更依赖 IdleConnTimeout 主动清理 + 客户端重试逻辑。
为什么 resp.Body.Close() 是复用前提
Go 的连接复用不是“发完请求就放回池”,而是“读完响应体并显式关闭后才归还”。漏掉这一步,连接会一直挂在 idle 状态,直到 IdleConnTimeout 到期——但期间它已无法被复用,等于池中少一个可用连接。
- 务必在
defer resp.Body.Close()前检查err,否则 panic 时Close()不执行 - 如果只读部分 body(如用
io.LimitReader),仍需调用Close(),否则底层连接不会释放 - 用
httputil.DumpResponse调试时,它内部已读完 body 并关闭,但生产代码不能依赖这个行为
健康检查不能靠 tcp keepalive 单打独斗
即使 TCPKeepAlive 成功,也不能说明服务端 HTTP 层还活着。真实场景中常见问题:
- 下游服务重启后,旧连接 TCP 层仍通,但发请求返回
408 Request Timeout或直接 EOF - LB 已摘除节点,但客户端池里还存着指向旧 IP 的连接
解决思路不是“每次请求前 ping 一次”,而是:
- 用
http.Transport.IdleConnMetrics()(Go 1.19+)定期采样Rejected计数,突增说明池中坏连接多 - 在关键业务路径上,对高频接口加轻量级健康探针(如
HEAD /health),失败时调用transport.CloseIdleConnections()清池 - 避免在
DialContext里做同步 HTTP 探活,会拖慢所有新建连接
真正难调的从来不是数字本身,而是确认连接到底有没有被复用——用 curl -v 看响应头里的 Connection: keep-alive 和 Reusing existing connection! 日志,比任何监控都直接。

















