http client默认连接池撑不住高并发,因maxidleconns等参数过小、idleconntimeout过短、未合理配置keepalive及超时分层,且易因body未关闭导致连接泄漏。

HTTP Client默认连接池为什么撑不住高并发
Go 的 http.DefaultClient 看似开箱即用,但它的底层 http.Transport 默认配置对高并发极不友好:
-
MaxIdleConns和MaxIdleConnsPerHost都是 100(Go 1.19+ 后升为 1000,但仍常不够) -
IdleConnTimeout默认 30 秒,连接空闲太久会被主动关掉,导致后续请求重新建连 - 没配
KeepAlive或设得太短,TCP 层无法复用连接
现象很典型:QPS 上去后,net/http: request canceled (Client.Timeout exceeded while awaiting headers) 或大量 connection refused / too many open files 错误,本质是连接耗尽或频繁握手拖垮性能。
- 真实压测中,未调优的 Client 在 500+ QPS 就可能开始超时,尤其后端响应稍慢(>200ms)时更明显
-
http.DefaultClient是全局单例,所有包共用同一 Transport,一不小心就被第三方库悄悄改掉配置 - Linux 系统级限制(如
ulimit -n)必须同步调大,否则 Go 再怎么配也打不开足够文件描述符
如何安全地自定义Transport并复用Client实例
别在每次请求都 new http.Client,也不要用 http.DefaultClient 直接改 Transport(会污染全局)。正确做法是:
- 创建独立的
http.Client实例,并传入自定义&http.Transport{} - 所有需要发请求的模块共享这个 Client 实例(比如通过依赖注入或包级变量)
-
Transport必须在首次使用前完整配置,之后不可修改(Go 不允许运行时改某些字段)
关键参数建议值(根据目标 QPS 和后端稳定性调整):
立即学习“go语言免费学习笔记(深入)”;
-
MaxIdleConns: 200(整个 Client 最大空闲连接数) -
MaxIdleConnsPerHost: 100(单个域名/IP 最大空闲连接数) -
IdleConnTimeout: 90 * time.Second(比后端反向代理的 keepalive timeout 小 10–20 秒) -
KeepAlive: 30 * time.Second(TCP 层心跳间隔,避免被中间设备断连) -
TLSHandshakeTimeout: 5 * time.Second(防止 TLS 握手卡死) -
ExpectContinueTimeout: 1 * time.Second(不用 expect/continue 场景可设小些)
超时控制必须分层设置,不能只靠Client.Timeout
Client.Timeout 是总超时,覆盖 DNS、连接、TLS、写请求、读响应全过程。一旦设太短,容易在连接池等待阶段就中断;设太长,又会让故障传播变慢。
更合理的做法是分层控制:
-
Transport.DialContext里控制 TCP 连接超时(通常 3–5 秒) -
Transport.TLSHandshakeTimeout单独控 TLS 握手(同上) -
Client.Timeout保留为兜底总时限(比如 15 秒),但日常请求应走 context 控制 - 实际发起请求时,用
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second),再传给client.Do(req.WithContext(ctx))
这样既避免连接池排队被总超时误杀,又能精准控制业务侧可接受的最长等待。
连接泄漏和资源未释放的隐蔽坑
最常被忽略的是:忘记关闭响应体(resp.Body.Close())。
不关会导致连接无法归还给连接池,空闲连接数持续下降,最终池子“干涸”,新请求只能等或新建连接——而新建又受 MaxIdleConns 限制,形成恶性循环。
- 每次
client.Do()后,必须确保resp.Body.Close()被执行,哪怕 resp 是 nil 或出错 - 推荐用
defer resp.Body.Close(),但要注意:如果 resp 为 nil(比如Do()返回 error),会 panic,所以得先判空 - 更稳妥写法:
if resp != nil { defer resp.Body.Close() }
- 如果用了
io.Copy或json.NewDecoder(resp.Body).Decode(),它们不会自动关 Body,仍需手动关
另外,http.Transport 本身不自动清理已关闭的连接,全靠 GC 和空闲超时回收,所以 IdleConnTimeout 不能设为 0,也不能无限长。
连接池不是黑盒,它依赖你关 Body、设对超时、配好系统限制。少一个环节,压测时就可能突然崩在第 3729 个请求上。










