根本原因是http.defaulttransport连接池过于保守:maxidleconnsperhost默认仅2、maxidleconns默认100、idleconntimeout默认30秒,导致高qps下频繁建连、dns/tls卡顿引发超时。

为什么默认的 http.DefaultClient 在压测时连接暴涨、超时频发
根本原因不是“没复用”,而是默认 http.DefaultTransport 的连接池太保守:每个域名(MaxIdleConnsPerHost)只允许 2 个空闲连接,全局最多 100 个,且空闲 30 秒就丢弃。QPS 超过 30 就开始排队建连,net/http: request canceled (Client.Timeout exceeded while awaiting headers) 实际是卡在 DNS 或 TLS 握手,不是业务逻辑慢。
常见误操作包括:
- 直接用
http.Get/http.Post—— 它们强制走http.DefaultClient,无法调参 - 每次请求都
&http.Client{}新建实例 —— 连接池彼此隔离,复用失效 - 漏关
resp.Body—— 连接被标记为“使用中”,永远不归还池子 - 服务端响应头含
Connection: close—— 客户端收到后主动断连,复用被拦截
必须显式配置的四个 Transport 参数
http.Transport 才是连接复用的实际管理者,不配等于裸奔。这四个值缺一不可,否则连接池形同虚设:
-
MaxIdleConns:全局最多保留多少空闲连接,建议 200~500(默认 100) -
MaxIdleConnsPerHost:每个host:port最多缓存几个空闲连接,**必须设!默认 2 是最大瓶颈**,建议与MaxIdleConns同量级(如都设 200) -
IdleConnTimeout:空闲连接最长存活时间,推荐 30~90 秒;设为 0 会永久驻留,NAT/LB 可能静默断连,下次复用时报read: connection reset by peer -
TLSHandshakeTimeout:TLS 握手最长等待时间,高延迟网络下建议设 10 秒;不设则用默认值,但显式写出更可控
示例配置:
client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 200,
MaxIdleConnsPerHost: 200,
IdleConnTimeout: 60 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
},
Timeout: 30 * time.Second,
}
如何让 DNS 解析不拖慢首字节时间(TTFB)
Go 默认每次新建连接都触发一次 DNS 查询,容器或云环境里 DNS 解析常达 200ms+,TTFB 直接拉高。解决办法是给 http.Transport 配一个带缓存的 Resolver:
- 不要依赖系统 resolver(如
/etc/resolv.conf),它不缓存 - 用 Go 原生
net.Resolver+PreferGo: true,再套一层Dialer控制底层 DNS 连接超时 - 缓存有效期建议设 30 秒(
LookupHost结果默认不带 TTL,靠本地缓存兜底)
关键代码片段:
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
d := net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}
return d.DialContext(ctx, network, addr)
},
}
transport := &http.Transport{
Resolver: resolver,
// 其他参数...
}
HTTP/2 下连接复用的隐藏行为差异
Go 1.6+ 默认启用 HTTP/2,但它复用逻辑和 HTTP/1.1 不同:只要 Host 和 TLS 配置一致,就会复用同一个 TCP 连接并发多路请求。但这也带来两个易忽略点:
-
MaxIdleConnsPerHost在 HTTP/2 下控制的是「每个 Host 的最大空闲流数」,不是连接数 —— 表面看设小了也够用,但若服务端有连接数限制(如 Nginx 的http2_max_concurrent_streams),仍可能触发排队 - 别碰
ForceAttemptHTTP2:已废弃,强行设true会让客户端忽略服务端是否真支持 HTTP/2,某些 LB(如老版本 Nginx)会直接 reset 连接,报错net/http: request canceled (Client.Timeout exceeded) - HTTP/2 自动降级到 HTTP/1.1 是透明的,但降级后复用逻辑立刻切回
MaxIdleConnsPerHost控制连接数,所以这个参数仍必须配
真正要检查的,是服务端是否返回 h2 协议(用 curl -I --http2 https://example.com 看响应头 Alt-Svc 或直接抓包确认)。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











