深入理解 Go HTTP 客户端配置:从 Time Wait 问题到解决方案 省流内容提要: 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` 状态。 项目的架构如下: ``` 用户查询 -> [APISvr] --http post--> influxdb_ip:port ``` 这个服务的主要功能是高频率地向外部提供数据查询,`APISvr` 每秒对 InfluxDB 发起上万次 HTTP POST 请求。大量的 `TIME_WAIT` 状态的出现,意味着有大量的连接正在被创建和断开。此外,`TIME_WAIT`会短暂地占用端口,严重时会使端口耗竭出现`can't assign requested address`的错误 值得一提的是,我在 Reddit 的一篇 [文章](https://www.reddit.com/r/golang/comments/1730po3/nethttp_can_someone_explain_how_the/) 中,也发现了与此类似的现象。 项目使用的 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` 的角色是进行连接管理,它包含了连接池和管理逻辑。具体在[这篇文章](https://www.cnblogs.com/charlieroro/p/11409153.html)中可找到更多的信息。 通过阅读 InfluxDB 客户端的代码,发现它直接创建了 `transport`: ```go 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的配置: ``` // 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` 配置得过小,则会引发大量的连接创建和销毁。 来自 大脸猪 写于 2024-01-18 13:12 -- 更新于2024-09-14 01:19 -- 1 条评论