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→ 连接可能长期挂起,耗尽资源 - DNS 解析默认同步阻塞,无缓存,高并发下
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建议设为略大于后端平均响应时间,太短导致反复建连,太长占用 fd - 务必设
Timeout,否则单个 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()
立即学习“go语言免费学习笔记(深入)”;
别忽略 DNS 缓存和连接复用的实际效果
即使设置了合理的 Transport,若目标域名解析慢或不稳定,DialContext 仍可能卡在 net.Resolver.LookupIPAddr。Go 1.18+ 支持自定义 Resolver,但更简单有效的方式是预热 + 固定 IP(适用于内网或固定后端)。
- 启动时用
net.DefaultResolver.LookupHost预解析关键域名,结果缓存到 map 中 - 对内网服务,直接构造
http://10.0.1.100:8080/path,绕过 DNS;配合Transport.DialContext强制使用该 IP - 观察
http.Transport.IdleConnMetrics(需 Go 1.21+)或通过 pprof 查看net/http.http2addConnIfNeeded调用频次,确认复用是否生效 - 注意:HTTP/2 连接复用粒度是整个 TCP 连接,不是 per-request,所以高并发下单连接承载能力远高于 HTTP/1.1
实际压测中,从默认 client 切到合理配置后,P95 延迟下降 60% 以上很常见,但前提是你的 goroutine 并发数、timeout、DNS、服务端限流这四点都对齐了——漏掉任意一环,优化都会打折扣。











