Go http.Client 的隐藏陷阱

5 阅读2分钟

Client 是 http 包内部发起请求的组件,使用它,我们才可以去控制请求的超时、重定向和其他的设置。以下是 Client 的定义:

type Client struct {
 Transport RoundTripper
 CheckRedirect func(req *Request, via []*Request) error
 Jar CookieJar
 Timeout time.Duration
}

要管理 HTTP 客户端的头域、重定向策略和其他设置, 需要创建一个 Client;要管理代理、TLS 配置、keep-alive、压缩和其他设置,需要创建一个 Transport。Client 和 Transport 类型都可以安全的被多个 go 程同时使用。出于效率考虑,应该一次建立、尽量重用。

1. 默认的 http.Client

默认的 http.Client 不包含请求超时时间,如果你使用 http.Get(url) 或者 &Client{} , 这将会使用 http.DefaultClient,这个结构体内 no timeout 。

假如发出请求的服务端 API 有问题:没有及时响应 httpclient 请求但是保持了连接, 在高并发情况下,打开的连接数会持续增长,最终导致客户端服务器资源到达瓶颈。

解决方案:不要使用默认的 http.Client , 总是为 http.Client 指定 Timeout 。

client := &http.Client{
  Timeout: 10 * time.Second,
 }

http.Client Timeout 包括连接、重定向(如果有)、从 Response Body 读取的时间,内置定时器会在 Get,Head、Post、Do 方法之后继续运行,直到读取完 Response.Body。

2. 默认的 http.Transport DefaultMaxIdleConnsPerHost 值

http.Transport 用于连接池化,客户端会尽量复用池中已经建立的 tcp 连接。连接池的创建是由首次请求来驱动的。

默认 DefaultTransport:

var DefaultTransport RoundTripper = &Transport{
 Proxy: ProxyFromEnvironment,
 DialContext: defaultTransportDialContext(&net.Dialer{
  Timeout:   30 * time.Second,
  KeepAlive: 30 * time.Second,
 }),
 ForceAttemptHTTP2:     true,
 MaxIdleConns:          100,
 IdleConnTimeout:       90 * time.Second,
 TLSHandshakeTimeout:   10 * time.Second,
 ExpectContinueTimeout: 1 * time.Second,
}

其中已赋值的字段:

MaxIdleConns:          100,                // 最大空闲连接数。默认 100。
IdleConnTimeout:       90 * time.Second,   // 空闲 keep-alive 连接在关闭之前保持空闲的时长。

及未显示赋值的其他字段取默认值:

DisableKeepAlives bool     // 默认 false 开启 keep-alive 。
MaxIdleConnsPerHost int    // 限制每个主机保留的最大空闲(保持活跃)连接数。默认 0 则使用 DefaultMaxIdleConnsPerHost = 2。
MaxConnsPerHost int        // 限制每个主机的连接总数,包括处于拨号、活动和空闲状态的连接。默认 0 没有限制。

http.Client 连接池化,能创建的连接是无限制的,每个 Host 能创建的连接 MaxConnsPerHost = 0 ,也是无限制的;有问题的是 DefaultMaxIdleConnsPerHost = 2,即连接池中每个主机的空闲连接数是2个,其实也就是每个主机能复用的连接数就是2个。

发现问题了吗?能无限制创建,但是能复用的只有2个。

这意味着:如果你的请求是高并发持续请求,一开始请求能无限制创建,但是由于不能复用 tcp 连接(2个,聊胜于无),造成客户端主动关闭 tcp 连接,time_wait 状态(2min)会占用大量端口,之后就不能发起 tcp 连接了。

解决方案:不要使用默认 Transport ,增加 MaxIdleConnsPerHost 。

 t := http.DefaultTransport.(*http.Transport).Clone()
 t.MaxIdleConnsPerHost = 100

 httpClient := &http.Client{
  Timeout:   10 * time.Second,
  Transport: t,
 }

References
www.loginradius.com/blog/engine…
xujiahua.github.io/posts/20200…
tonybai.com/2021/04/02/…
tonybai.com/2021/01/08/…