MaxIdleConnsPerHost=2 是每个 host 最大空闲连接数,非并发上限;高 QPS 下易致连接复用率低、TIME_WAIT 暴涨、频繁新建连接;应按预期并发的 1/3~1/2 设置,并配合 IdleConnTimeout(建议设为下游 keepalive_timeout 最小值减 10s)、DialContext 等分层超时及全局复用 http.Client。

MaxIdleConnsPerHost=2 为什么总在高 QPS 下崩
这个值不是“并发上限”,而是「每个 host(含端口)最多缓存几个空闲连接」。默认 2 意味着:哪怕你稳定压测 60 QPS,只要全打向 https://api.pay.example.com,95% 的请求都等不到复用,只能排队或新建连接。
典型现象包括:net/http: request canceled (Client.Timeout exceeded while awaiting headers)、TIME_WAIT 连接暴涨、strace 看到大量 connect() 系统调用。
误判常发生在排查 DNS 或 TLS 握手失败时——实际是连接刚用完就被关,下次还得重连。
- 对固定后端(如支付网关、内部服务),建议设为预期并发的
1/3~1/2;例如稳定60QPS,可配MaxIdleConnsPerHost: 25 - 如果同时调用
50+不同域名,必须同步放大MaxIdleConns(全局总数),否则某几个 host 把池子占满,别的 host 就拿不到连接 - 设太大(如 >100)会拖慢失效连接清理,尤其当下游 LB 切 IP 或服务重启后,旧连接卡在池里继续报
read: connection reset by peer
IdleConnTimeout 设太短或太长都会让连接池失效
IdleConnTimeout 控制空闲连接在池里躺多久才被关掉。Go 1.19+ 默认 90 秒,但很多云网关(Nginx/ALB/Envoy)的 keepalive_timeout 是 60~75 秒——客户端设得比它还长,就会复用到已被服务端 RST 的连接。
太短(如 5s):连接刚缓存就过期,复用率为零,全是短连接;太长(如 5m):下游变更后旧连接持续失败,错误日志反复刷屏。
- 推荐做法:查清下游所有网关的
keepalive_timeout,取最小值减去10秒;常见设为60 * time.Second或90 * time.Second - 必须和
Dialer.KeepAlive对齐,比如设KeepAlive: 30 * time.Second,避免中间设备主动断连 - 别依赖
Client.Timeout覆盖它——那是总超时,不干预连接池内部行为
别碰 http.DefaultClient,也别每次请求都 new(http.Client{})
http.DefaultClient 是全局单例,底层共享 http.DefaultTransport。任何第三方包(比如 cloud.google.com/go 或 go.opentelemetry.io)修改它的 Transport,都会静默污染你的请求行为。
而每次请求都写 client := &http.Client{Transport: myTransport},等于每个 client 持有独立 Transport 实例,idle connection goroutine 不退出,内存和 fd 持续上涨——线上见过一秒几千请求把内核 FD 撑爆的案例。
- 正确做法:定义一个全局复用的
*http.Client变量,显式配置Transport - 确保
Transport的DisableKeepAlives是false(默认值),否则连接池直接被禁用 - 别在
Do后忘了resp.Body.Close(),否则连接无法归还池中
超时必须分层配,不能只靠 Client.Timeout
Client.Timeout 是总超时,覆盖建连、重定向、读响应体全过程,但它无法中断正在发生的 TCP 建连或 TLS 握手。一旦 DNS 卡住、服务端不发响应头,goroutine 就永久阻塞。
真正影响连接池健康度的是 Transport 的分阶段超时:
-
DialContext(含 DNS 解析 + TCP 连接):建议3~5s;设太长会让失败连接卡住整个池 -
TLSHandshakeTimeout:HTTPS 场景下必须单独设,同样建议 ≤5s -
ResponseHeaderTimeout:从发完请求到收到第一个字节响应头的时间,防后端“只写不发” -
ExpectContinueTimeout:仅当使用Expect: 100-continue时生效,一般不用动


















