省流内容提要:
- 长连接出现大量的
TIME_WAIT
状态,这通常是由于 HTTP 中的transport
配置MaxIdleConnsPerHost/MaxIdleConns
设置不当所导致的。次要原因是,go必须将连接中的数据使用io.ReadAll
读完才能Close,否则连接也不会复用。 - http client中需要传入transport,其中有配置
MaxIdleConnsPerHost/MaxIdleConns
,这些配置非常重要,在大吞吐的客户端上可以理解为客户端维持的最终连接数。MaxIdleConnsPerHost默认=2,可能太保守。 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
:
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 客户端,在短时间内向另一个服务发起数千次请求,会发生以下情况:
- 虽然 HTTP 1.1 连接可以keep alive,但不能多路复用,这会创建大量的连接。如果是 HTTP 2,就不会有这个问题,这也是为什么 gRPC 只需要一个连接就能维持很高的吞吐量。
- 在请求结束后,由于暂时没有发送/接收数据,transport会认为连接已经空闲。而默认的最大空闲连接数为 2,这导致只会保留 2 个连接,而将其他的全部主动关闭。
在 TCP 中,主动关闭连接的一方最终会进入 TIME_WAIT
状态。虽然 TIME_WAIT
本身并没有什么害处,但大量的连接创建和销毁会增加性能的开销。
因此,MaxIdleConnsPerHost
是一个非常重要的配置,与 HTTP 客户端的性能密切相关。在向同一个服务发起大量请求的客户端上,MaxIdleConnsPerHost
可以理解为客户端维持的最终连接数。在执行 netstat -anp|grep EST|wc -l
时,你会发现 EST
状态的连接数和 MaxIdleConnsPerHost
差不多。
通常,MaxConnsPerHost
的配置应等于或略大于 MaxIdleConnsPerHost
。如果这个值配置得过小,当连接达到阈值时,会阻塞连接的创建并进行等待,从而影响网络的吞吐量。如果 MaxConnsPerHost
配置得过大,而 MaxIdleConnsPerHost
配置得过小,则会引发大量的连接创建和销毁。