Golang并发HTTP请求的性能优化实践

11次阅读

http.DefaultClient在高并发下易成瓶颈,因其默认连接池参数过小(MaxIdleConnsPerHost=0)、dns同步阻塞、缺少超时控制;需自定义Client配置连接复用、超时、并发限流及DNS优化。

Golang并发HTTP请求的性能优化实践

为什么 http.DefaultClient 在高并发下容易成为瓶颈

默认的 http.DefaultClient 使用的是共享的 http.Transport,其底层连接池(MaxIdleConnsMaxIdleConnsPerHost)默认值极低(通常为 2),在并发请求密集时会频繁新建 TCP 连接、等待空闲连接,甚至触发 DNS 查询阻塞。这不是代码写得“错”,而是默认配置根本没为并发场景准备。

  • MaxIdleConns 默认为 100go 1.19+),但旧版本是 0 → 实际禁用空闲连接复用
  • MaxIdleConnsPerHost 默认为 0 → 每个域名最多 0 个空闲连接,等于每次都要建连
  • 未设置 IdleConnTimeoutTLSHandshakeTimeout → 连接可能长期挂起,耗尽资源
  • 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.WaitGroup 

for , 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、服务端限流这四点都对齐了——漏掉任意一环,优化都会打折扣。

text=ZqhQzanResources