
http.DefaultClient在高并发下易成瓶颈,因其默认连接池参数过小(MaxIdleConnsPerHost=0)、DNS同步阻塞、缺少超时控制;需自定义Client配置连接复用、超时、并发限流及DNS优化。
http.DefaultClient 在高并发下容易成为瓶颈默认的 http.DefaultClient 使用的是共享的 http.Transport,其底层连接池(MaxIdleConns、MaxIdleConnsPerHost)默认值极低(通常为 2),在并发请求密集时会频繁新建 TCP 连接、等待空闲连接,甚至触发 DNS 查询阻塞。这不是代码写得“错”,而是默认配置根本没为并发场景准备。
MaxIdleConns 默认为 100(Go 1.19+),但旧版本是 0 → 实际禁用空闲连接复用MaxIdleConnsPerHost 默认为 0 → 每个域名最多 0 个空闲连接,等于每次都要建连IdleConnTimeout 和 TLSHandshakeTimeout → 连接可能长期挂起,耗尽资源lookup 成为隐性瓶颈http.Client 实现稳定吞吐关键不是“换 client”,而是显式控制连接生命周期和复用策略。下面这个配置在多数内网/云环境能支撑 500–2000 QPS 稳定运行:
client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 1000,
MaxIdleConnsPerHost: 1000,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
// 可选:启用 HTTP/2(Go 1.6+ 默认开启,但需服务端支持)
// ForceAttemptHTTP2: true,
},
Timeout: 15 * time.Second,
}MaxIdleConnsPerHost 设为和 MaxIdleConns 相同值,避免跨域名争抢连接池IdleConnTimeout 建议设为略大于后端平均响应时间,太短导致反复建连,太长占用 fdTimeout,否则单个 hang 请求会拖垮整个 goroutine 池MaxIdleConns 到 10000+ —— 受限于系统 ulimit -n,且可能触发服务端限流semaphore 而非无限制 go 启动直接 for range urls { go doRequest(...) } 是最常见误操作:goroutine 数量失控,内存暴涨,调度器过载,还可能被目标服务主动断连或限速。
golang.org/x/sync/semaphore 控制并发度,例如限制最多 50 个并发请求context.WithTimeout,避免 semaphore 令牌被长期占住sem := semaphore.NewWeighted(50) var wg sync.WaitGroupfor , url := range urls { if err := sem.Acquire(ctx, 1); err != nil { log.Printf("acquire failed: %v", err) continue } wg.Add(1) go func(u string) { defer sem.Release(1) defer wg.Done() req, := http.NewRequestWithContext(ctx, "GET", u, nil) resp, err := client.Do(req) // ... 处理 resp / err }(url) }
wg.Wait()
即使设置了合理的 Transport,若目标域名解析慢或不稳定,DialContext 仍可能卡在 net.Resolver.LookupIPAddr。Go 1.18+ 支持自定义 Resolver,但更简单有效的方式是预热 + 固定 IP(适用于内网或固定后端)。
net.DefaultResolver.LookupHost 预解析关键域名,结果缓存到 map 中http://10.0.1.100:8080/path,绕过 DNS;配合 Transport.DialContext 强制使用该 IPhttp.Transport.IdleConnMetrics(需 Go 1.21+)或通过 pprof 查看 net/http.http2addConnIfNeeded 调用频次,确认复用是否生效实际压测中,从默认 client 切到合理配置后,P95 延迟下降 60% 以上很常见,但前提是你的 goroutine 并发数、timeout、DNS、服务端限流这四点都对齐了——漏掉任意一环,优化都会打折扣。