最近有一个压测的任务,首先使用gin写了一个http server,将请求数据写入到mysql,写入qps需要达到20K,当然,为了保险起见,使用了自己写的一个http client进行了压力测试,qps可以达到23K-25K的样子,然后就把这个http server部署到测试环境进行测试。
对于golang的http client,设置的MaxConnsPerHost为3000,使用九个client进行测试,结果发现http server的端口占用很高,然后查看了client端的端口占用,发现处于ESTABLISHED状态的都远远超过3000,之后就使用dlv进行测试,然后从golang官网下载了go1.15.6,在调试过程中,发现调试展示的代码为dlv所在的golang的版本,而运行的程序基于我本地的go1.12.7,所以显示的结果有很多出乎意料的地方,例如Transport.connsPerHost map中的value为负数的情况,这个,也让我觉得go1.12.7可能在MaxConnsPerHost设置效果方面存在问题。但是,无论如何,都需要先把程序编译的golang版本和dlv调试用的版本对齐,go1.15已经发布了大约5个月, 改动也没有很多,稳定性比较可靠,就把编译用的golang版本改为go1.15.6。重新编译运行之后,client端处于ESTABLISHED的数量一直都低于3000,这个问题解决了。
虽然处于ESTABLISEHD的端口号占用一直不超过3000,但是netstat -antp中显示与client相关的端口号却经常远远超过3000,其中很多是如下显示:
对tcp四次挥手协议比较了解的,应该知道,这很有可能是client发起了关闭tcp连接的请求,但是server端没有关闭tcp连接导致的。查看http server,发现了有大量的CLOSE_WAIT,与设想相同。但是查看MaxConnsPerHost的描述:
// 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 // Go 1.11
MaxConnsPerHost会限制处于dialing,active,idle状态的tcp连接数。如果连接数已经达到限制,就会阻塞。也就是说,MaxConnsPerHost并不会导致很多的tcp连接关闭。然后,猜测,这个会不会与Transport.DialContext.KeepAlive的时间有关,测试的时候,我们调大了KeepAlive的时间,也猜测可能与Transport.IdleConnTimeout有关,于是也调大了这个参数。发现还是会有很多的FIN_WAIT2状态出现。于是,继续使用dlv进行调试,因为FIN_WAIT2的出现必定和tcp关闭有关,于是我们在关闭连接相关的地方加上断点,发现了如下的调用堆栈结果:
按照上述调用堆栈,可以知道,在一个请求被取消的时候,对应的tcp连接会被关闭。这里请求被取消是因为超时,超时说明http server太繁忙,而http server端出现很多的CLOSE_WAIT,也说明服务端太繁忙,这两点比较契合。但,很重要的一点是,就算设置了MaxConnsPerHost,如果出现很多的请求超时,一样会导致客户端的端口号占用增加,以及服务端需要打开的文件数变多。