bigywater.tech
Go · 88

http.Client 连接池:Transport 的空闲连接和泄漏

http.Client 的连接池藏着几个常见的坑:响应体没读完、MaxIdleConnsPerHost 默认只有 2、每次新建 Transport、默认客户端没有超时。这篇用服务端的 ConnState 回调数连接,用 goroutine 数量看泄漏,把每个坑量出来。

1 vs 20
20 个串行请求用的连接数:读完并关闭 vs 既不读也不关
18
MaxIdleConnsPerHost 默认值下,第二波 20 个并发请求新建的连接数
150 个
每个请求新建一个 Transport 时,50 个请求后多出的 goroutine

响应体必须读完并关闭

实测 · 本机执行输出
  响应体    1000 字节,读完并关闭   :20 个串行请求,服务端新建连接  1 个;客户端 goroutine 增加 3
  响应体    1000 字节,只关闭不读   :20 个串行请求,服务端新建连接  1 个;客户端 goroutine 增加 3
  响应体    1000 字节,既不读也不关  :20 个串行请求,服务端新建连接 20 个;客户端 goroutine 增加 60
  响应体 1048576 字节,读完并关闭   :20 个串行请求,服务端新建连接  1 个;客户端 goroutine 增加 3
  响应体 1048576 字节,只关闭不读   :20 个串行请求,服务端新建连接 20 个;客户端 goroutine 增加 0
  响应体 1048576 字节,既不读也不关  :20 个串行请求,服务端新建连接 20 个;客户端 goroutine 增加 63

读完并 Close,20 个请求用 1 个连接。响应体很小(1000 字节)时只 Close 也能复用;响应体大(1 MiB)只 Close 就会关掉连接,20 个请求新建 20 个连接。既不读也不 Close 最糟:20 个连接各占 3 个 goroutine,一直留着。习惯是 io.Copy(io.Discard, resp.Body) 再 Close。

空闲连接的数量

实测 · 本机执行输出
  默认(0 = 使用 DefaultMaxIdleConnsPerHost)    第一波 20 个并发请求新建连接 20 个;间隔 100 ms 后第二波 20 个并发请求,又新建 18 个
  MaxIdleConnsPerHost = 20                 第一波 20 个并发请求新建连接 20 个;间隔 100 ms 后第二波 20 个并发请求,又新建 0 个
  http.DefaultMaxIdleConnsPerHost 的值 = 2

MaxIdleConnsPerHost 的默认值是 2(DefaultMaxIdleConnsPerHost),所以一波 20 个并发请求结束后只留 2 个空闲连接,下一波又新建 18 个;调到 20 就全部复用。对同一个主机并发较高的服务,这个值值得调。

Transport 要共用

实测 · 本机执行输出
  每个请求新建一个 Transport(并且不 CloseIdleConnections):50 个请求之后,服务端新建连接 50 个、仍未关闭 50 个;客户端 goroutine 增加 150
  共用一个 Transport:50 个请求之后,服务端新建连接 1 个、仍未关闭 1 个;客户端 goroutine 增加 3

每个请求新建一个 Transport,每个都有自己的连接池,50 个请求后服务端有 50 个未关闭的连接、客户端多了 150 个 goroutine。共用一个 Transport,50 个请求只用 1 个连接。

超时

实测 · 本机执行输出
  Client.Timeout = 300 ms,服务器接受连接后不回应:301 ms 后返回错误:Get "http://127.0.0.1:55889": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
  http.DefaultClient.Timeout = 0s(0 表示没有超时)

http.DefaultClient 的 Timeout 是 0,没有超时;服务器接受连接后不回应,请求会一直挂着。自己建 Client 并设 Timeout,实验里 301 ms 后返回 “Client.Timeout exceeded while awaiting headers”。

默认值

实测 · 本机执行输出
  DefaultTransport:MaxIdleConns=100 MaxIdleConnsPerHost=0(0=用默认值 2) IdleConnTimeout=1m30s TLSHandshakeTimeout=10s ExpectContinueTimeout=1s ForceAttemptHTTP2=true MaxConnsPerHost=0

DefaultTransport:MaxIdleConns 100,IdleConnTimeout 90 秒,TLSHandshakeTimeout 10 秒,ForceAttemptHTTP2 为真,MaxConnsPerHost 不限制。

分步演示

http.Client 连接池
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。服务端和客户端在同一进程里,服务端用 http.Server.ConnState 数 StateNew 与 StateClosed;客户端 goroutine 增量用 runtime.NumGoroutine 前后差;每个设置单独跑一遍;“既不读也不关”“每次新建 Transport”的 goroutine 数量依赖实现,只代表 Go 1.27.1
    • 没有验证:HTTPS/HTTP2 下的连接复用(HTTP/2 多路复用时 MaxIdleConnsPerHost 的意义不同)、IdleConnTimeout 到期后的回收、MaxConnsPerHost 限制的行为、代理、请求取消时连接的处理、Linux 上的差别
    京ICP备2026067502号-1京公网安备11010502063487号