根本原因是maxidleconnsperhost默认为2导致连接无法复用,且resp.body未读完就close破坏复用逻辑;必须同步调大maxidleconnsperhost、maxconnsperhost,并确保完整消费响应体。

Go HTTP 客户端用长连接却爆出海量 TIME_WAIT,根本不是“长连接没用好”,而是连接池配置和响应体处理同时失守。
MaxIdleConnsPerHost = 2 是默认陷阱
Go 的 http.Transport 默认 MaxIdleConnsPerHost 为 2,意味着每个目标域名(如 api.example.com:443)最多只缓存 2 个空闲连接。并发请求一超过这个数(比如 50 QPS),其余请求只能新建连接 —— 每次新建 + 主动关闭,就触发一次 TIME_WAIT。
- 单后端场景(如只连 InfluxDB 或自建 API),建议设为
MaxIdleConnsPerHost = 100或更高,与峰值 QPS 匹配 - 必须同步设置
MaxConnsPerHost >= MaxIdleConnsPerHost,否则连接池会阻塞等待,反而引发超时 - 避免
MaxConnsPerHost远大于MaxIdleConnsPerHost:前者管上限,后者管常驻;失配会导致连接建了不用、立刻销毁,加剧 TIME_WAIT
resp.Body.Close() ≠ 读完响应体
调用 resp.Body.Close() 只是关掉 body 流,不等于消费了数据。Go 的复用逻辑要求:连接上所有响应数据必须被完整读取或丢弃,否则 transport 认为连接可能残留未读内容,拒绝复用,强制关闭连接。
- 无论是否需要 body 内容,都必须显式处理:
io.Copy(io.Discard, resp.Body)(Go 1.19+) - 若解析 JSON,
json.NewDecoder(resp.Body).Decode(&v)内部会读完流;但 decode 失败后仍要补上io.Copy(io.Discard, resp.Body) - 切勿只写
defer resp.Body.Close()就以为万事大吉 —— 关闭不等于读完
别碰 http.DefaultClient
直接用 http.Get() 或 http.DefaultClient.Do() 等价于把命交给默认 transport:全局 MaxIdleConns = 100(非 per-host),且无法针对性调优。线上高并发服务中,这几乎必然导致连接池瓶颈。
- 务必显式构造
*http.Client,传入自定义http.Transport - 不同目标 domain(如 S3、本地服务、第三方 API)应分 client 配置,各自独立 transport
- 注意
IdleConnTimeout(默认 90s):太短导致空闲连接过早淘汰;太长则 fd 占用久;建议设为 30–60s,结合业务 RTT 调整
真正难处理的不是参数值本身,而是「body 是不是真被读完了」这个动作 —— 它发生在每次请求末尾,不可见、易忽略、后果严重。只要漏掉一次,复用链就断了,TIME_WAIT 就开始堆积。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











