go http客户端需显式配置各阶段超时、合理设置连接池参数、匹配服务端keepalive时长、及时关闭response.body,http/2下需用maxconnsperhost而非maxidleconnsperhost控流。

Timeout 设置不全,连接会卡死在 DNS 或 TLS 阶段
Go 的 http.Client 默认只设了 Timeout,但这个字段只控制整个请求的总耗时,对底层连接建立(比如 DNS 解析、TCP 握手、TLS 协商)完全不生效。高并发下,一旦 DNS 慢或服务端 TLS 证书异常,大量 goroutine 就卡在 net.DialContext 里,连接池被占满却无法复用。
必须显式配置 Transport 的各阶段超时:
-
DialContext用&net.Dialer{Timeout: 5 * time.Second, KeepAlive: 30 * time.Second} -
TLSHandshakeTimeout设为5 * time.Second(不能依赖全局Timeout) -
ExpectContinueTimeout建议设为1 * time.Second,避免 POST 大体请求卡住
MaxIdleConns 和 MaxIdleConnsPerHost 搞反了就白调
这两个参数不是“越大越好”,也不是“设一样就行”。MaxIdleConns 是整个 client 能保持的空闲连接总数;MaxIdleConnsPerHost 是单个 host(比如 api.example.com)最多缓存几个空闲连接。如果只调大前者但后者还是默认的 2,那再多的连接池容量也分不到每个域名上。
典型误配:
立即学习“go语言免费学习笔记(深入)”;
- 压测时发现复用率低、新建连接多 → 先看
http.DefaultClient.Transport.IdleConnMetrics(Go 1.19+)或打日志观察idleConn数量 - 若目标是少数几个后端服务,建议
MaxIdleConnsPerHost = 100,MaxIdleConns = 1000 - 若请求散落在上百个域名,
MaxIdleConnsPerHost要压到 10~20,否则内存和文件描述符容易爆
KeepAlive 时间比服务端更短,连接会被悄悄断掉
HTTP/1.1 的长连接靠 TCP keepalive 维持,但客户端和服务端各自独立控制。如果 client 的 KeepAlive 是 30s,而 Nginx 的 keepalive_timeout 是 75s,看起来没问题;但反过来——client 设 90s,Nginx 设 60s,那第 61 秒起,服务端就可能发 RST,client 还以为连接可用,下次复用时直接报 read: connection reset by peer。
实操建议:
- 查清下游所有 LB / 网关的 keepalive 配置(Nginx、Envoy、ALB 等),取其中最小值减去 5~10s 作为 client 的
Dialer.KeepAlive - 加上
IdleConnTimeout: 90 * time.Second(必须 ≥ KeepAlive,否则空闲连接提前被关) - 别忘了
Response.Body一定要Close(),否则连接永远不会进 idle 状态
HTTP/2 下 MaxConnsPerHost 不起作用,得换思路
Go 1.6+ 默认启用 HTTP/2,此时 MaxIdleConnsPerHost 对 HTTP/2 连接无效——一个 HTTP/2 连接能复用所有 stream,所以 Go 把它当做一个连接来管理,不受 per-host 限制。但如果你混用 HTTP/1.1 和 HTTP/2,或者某些中间件强制降级,行为就会不一致。
应对方式很直接:
- 确认是否真需要 HTTP/2:不需要就关掉——
Transport.ForceAttemptHTTP2 = false - 需要 HTTP/2 且要控流:用
MaxConnsPerHost(注意不是MaxIdleConnsPerHost),它对 HTTP/2 生效,表示“每个 host 最多建几个底层 TCP 连接” - HTTP/2 下还要留意
MaxIdleConns仍是全局总连接数上限,别设太高导致 fd 耗尽
最常被忽略的是:HTTP/2 的连接复用粒度变了,但你的监控指标(比如每秒新建连接数)还在按 HTTP/1.1 的逻辑理解,结果误判连接池没生效。










