http.Client 的连接池藏着几个常见的坑:响应体没读完、MaxIdleConnsPerHost 默认只有 2、每次新建 Transport、默认客户端没有超时。这篇用服务端的 ConnState 回调数连接,用 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(并且不 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 不限制。