深入理解 Go HTTP 客户端配置:从 Time Wait 问题到解决方案

2024-01-19 14:33:18 浏览数 (2)

省流内容提要:

  1. 长连接出现大量的 TIME_WAIT 状态,这通常是由于 HTTP 中的 transport 配置 MaxIdleConnsPerHost/MaxIdleConns 设置不当所导致的。次要原因是,go必须将连接中的数据使用io.ReadAll读完才能Close,否则连接也不会复用。
  2. http client中需要传入transport,其中有配置MaxIdleConnsPerHost/MaxIdleConns,这些配置非常重要,在大吞吐的客户端上可以理解为客户端维持的最终连接数。MaxIdleConnsPerHost默认=2,可能太保守。
  3. MaxConnsPerHost一般配置为等于或略大于MaxIdleConnsPerHost,这个值如果配置的过小,在连接达到阈值,会阻塞连接的创建进行等待。从而影响网络的吞吐。如果 MaxConnsPerHost 配置得过大,而 MaxIdleConnsPerHost 配置得过小,则会引发大量的连接创建和销毁造成TIME_WAIT

最近,我在项目中发现查询 InfluxDB 的模块出现了大量的 TIME_WAIT 状态。

项目的架构如下:

代码语言:javascript复制
用户查询 -> [APISvr] --http post--> influxdb_ip:port

这个服务的主要功能是高频率地向外部提供数据查询,APISvr 每秒对 InfluxDB 发起上万次 HTTP POST 请求。大量的 TIME_WAIT 状态的出现,意味着有大量的连接正在被创建和断开。此外,TIME_WAIT会短暂地占用端口,严重时会使端口耗竭出现can't assign requested address的错误

值得一提的是,我在 Reddit 的一篇 文章 中,也发现了与此类似的现象。

项目使用的 InfluxDB 客户端(github.com/influxdata/influxdb1-client/v2)已经有些年头了。它的工作原理相当直接:通过创建 Go 标准库中的 HTTP 客户端,对 InfluxDB 的 HTTP API 发起 POST 请求。出现大量的 TIME_WAIT 状态,这通常是由于 HTTP 中的 transport 配置 MaxIdleConnsPerHost/MaxIdleConns 设置不当所导致的。其次go http client还有一个坑,必须将连接中的数据使用io.ReadAll读完才能Close,否则连接也不会复用。

在 HTTP 客户端中,transport 的角色是进行连接管理,它包含了连接池和管理逻辑。具体在这篇文章中可找到更多的信息。

通过阅读 InfluxDB 客户端的代码,发现它直接创建了 transport

代码语言:javascript复制
tr := &http.Transport{
    TLSClientConfig: &tls.Config{
        InsecureSkipVerify: conf.InsecureSkipVerify,
    },
    Proxy: conf.Proxy,
}
if conf.TLSConfig != nil {
    tr.TLSClientConfig = conf.TLSConfig
}
...
return &client{
    ...
    transport: tr,
    encoding:  conf.WriteEncoding,
}, nil

令人惊讶的是,它没有提供给用户配置 MaxIdleConnsPerHost/MaxIdleConns 的方法。通过fork这份代码完善,在项目中将这两个配置都改成合适的数字,TIME WAIT暴增就解决了。但这是一个值得深入探究的问题。

transport的配置:

代码语言:javascript复制
    // MaxIdleConnsPerHost, if non-zero, controls the maximum idle
    // (keep-alive) connections to keep per-host. If zero,
    // DefaultMaxIdleConnsPerHost is used.
    MaxIdleConnsPerHost int

    // MaxConnsPerHost optionally limits the total number of
    // connections per host, including connections in the dialing,
    // active, and idle states. On limit violation, dials will block.
    //
    // Zero means no limit.
    MaxConnsPerHost int

默认的 DefaultMaxIdleConnsPerHost 为 2,这是一个相当保守的配置。而 MaxConnsPerHost 则为无穷大。如果服务作为 HTTP 客户端,在短时间内向另一个服务发起数千次请求,会发生以下情况:

  1. 虽然 HTTP 1.1 连接可以keep alive,但不能多路复用,这会创建大量的连接。如果是 HTTP 2,就不会有这个问题,这也是为什么 gRPC 只需要一个连接就能维持很高的吞吐量。
  2. 在请求结束后,由于暂时没有发送/接收数据,transport会认为连接已经空闲。而默认的最大空闲连接数为 2,这导致只会保留 2 个连接,而将其他的全部主动关闭。

在 TCP 中,主动关闭连接的一方最终会进入 TIME_WAIT 状态。虽然 TIME_WAIT 本身并没有什么害处,但大量的连接创建和销毁会增加性能的开销。

因此,MaxIdleConnsPerHost 是一个非常重要的配置,与 HTTP 客户端的性能密切相关。在向同一个服务发起大量请求的客户端上,MaxIdleConnsPerHost 可以理解为客户端维持的最终连接数。在执行 netstat -anp|grep EST|wc -l 时,你会发现 EST 状态的连接数和 MaxIdleConnsPerHost 差不多。

通常,MaxConnsPerHost 的配置应等于或略大于 MaxIdleConnsPerHost。如果这个值配置得过小,当连接达到阈值时,会阻塞连接的创建并进行等待,从而影响网络的吞吐量。如果 MaxConnsPerHost 配置得过大,而 MaxIdleConnsPerHost 配置得过小,则会引发大量的连接创建和销毁。

0 人点赞