一、产生大量ESTABLISHED
resp, err := getHttpClientIns().Get(url)
if err != nil {
return
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf(fmt.Sprintf("http code : %d", resp.StatusCode))
}
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
defer resp.Body.Close()
以上代码看起来貌似没什么问题,但是当resp.StatusCode != 200 的时候,直接返回了,因此 defer resp.Body.Close() 这句代码并没有被调用,也就是产生的tcp连接并没有被关闭。由于代码中会并发的多次请求,如果请求全部都不是200,那么最终都不会Close,便会产生大量的ESTABLISHED状态。
那么我们只需要把defer resp.Body.Close()这一句提前即可,如下:
resp, err := getHttpClientIns().Get(url)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf(fmt.Sprintf("http code : %d", resp.StatusCode))
}
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
当然,上面的代码其实依然有问题,后面再说。
二、产生大量的TIME_WAIT
前面说了,当时是并发的去大量的请求,代码类似下面:
for i := 0; i < 10; i++{
go func(){
for i := 0; i < 10000000; i++{
resp, err := http.Get(url)
}
}()
}
即开多个线程并发的去请求,然后就发现TIME_WAIT的数量持续上升,很快就导致系统资源耗尽。经查,发现http client的参数MaxConnsPerHost比较小,默认为2,因此调大了连接池的数量。具体请求的代码改成类型下面的样子
timeoutContext, _ := context.WithTimeout(context.Background(), timeOut)
req, err := http.NewRequestWithContext(timeoutContext, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := getHttpClientWithMaxPerlHost(maxPerlHostConnect).Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf(fmt.Sprintf("http code : %d", resp.StatusCode))
}
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
其中getHttpClientWithMaxPerlHost()函数主要是得到一个http clien,我是这样配的:
client = &http.Client{Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
//MaxIdleConns: maxPerlHostConnect*10,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
MaxConnsPerHost: maxPerlHostConnect,
MaxIdleConnsPerHost: maxPerlHostConnect / 2,
},
正常情况下,确实可以把TIME_WAIT的数量将下来。但是,当请求全部404之后,我发现仍然会产生大量TIME_WAIT,与正常的有啥不同呢,正常情况下会读取Body里面的内容。
正好网上的一篇文章使用golang的`http.Client`容易出现TIME_WAIT上涨的几种情况和解决方案 - Go语言中文网 - Golang中文社区 (studygolang.com),其中第一点引起了我的注意,我试着当resp.StatusCode != 200的时候,也把Body里面的内容读出来,代码如下:
timeoutContext, _ := context.WithTimeout(context.Background(), timeOut)
req, err := http.NewRequestWithContext(timeoutContext, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := getHttpClientWithMaxPerlHost(maxPerHost).Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
io.Copy(ioutil.Discard, resp.Body)
return nil, fmt.Errorf(fmt.Sprintf("http code : %d", resp.StatusCode))
}
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
果然,TIME_WAIT的数量降下来了。
总结:
1、如果单线程请求,默认的http client就够用了,不需要额外的参数配置。
2、如果多线程并发请求,那么需要配置参数,主要是MaxConnsPerHost和MaxIdleConnsPerHost
3、一定要记得调用resp.Body.Close(),关闭打开的Body.
4、一定要把Body里面的内容读出来,就算异常也要读出来丢掉。