golang 高并發下 tcp 建連數暴漲的原因分析
背景:服務需要高頻發出GET請求,然后我們封裝的是 golang 的net/http 庫, 因為開源的比如req 和gorequsts 都是封裝的net/http ,所以我們還是選用原生(req 使用不當也會掉坑里)。我們的場景是多協程從chan 中取任務,并發get 請求,然后設置超時,設置代理,完了。我們知道net/http 是自帶了連接池的,能自動回收連接,但是,發現連接暴漲,起了1萬個連接。
首先,我們第一版的代碼是基于python 的,是沒有連接暴漲的問題的,封裝的requests,封裝如下:
def fetch(self, url, body, method, proxies=None, header=None):
res = None
timeout = 4
self.error = ''
stream_flag = False
if not header:
header = {}
if not proxies:
proxies = {}
try:
self.set_extra(header)
res = self.session.request(method, url, data=body, headers=header, timeout=timeout, proxies=proxies)
# to do: self.error variable to logger
except requests.exceptions.Timeout:
self.error = "fetch faild !!! url:{0} except: connect timeout".format(url)
except requests.exceptions.TooManyRedirects:
self.error = "fetch faild !!! url:{0} except: redirect more than 3 times".format(url)
except requests.exceptions.ConnectionError:
self.error = "fetch faild !!! url:{0} except: connect error".format(url)
except socket.timeout:
self.error = "fetch faild !!! url:{0} except: recv timetout".format(url)
except:
self.error = "fetch faild !!! url:{0} except: {1}".format(url, traceback.format_exc())
if res is not None and self.error == "":
self.logger.info("url: %s, body: %s, method: %s, header: %s, proxy: %s, request success!", url, str(body)[:100], method, header, proxies)
self.logger.info("url: %s, resp_header: %s, sock_ip: %s, response success!", url, res.headers, self.get_sock_ip(res))
else:
self.logger.warning("url: %s, body: %s, method: %s, header: %s, proxy: %s, error: %s, reuqest failed!", url, str(body)[:100], method, header, proxies, self.error)
return res
改用golang后,我們選擇的是net/http。看net/http 的文檔,最基本的請求,如get,post 可以使用如下的方式:
resp, err := http.Get("http://example.com/")
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
resp, err := http.PostForm("http://example.com/form",url.Values{"key": {"Value"}, "id": {"123"}})
我們需要添加超時,代理和設置head 頭,官方推薦的是使用client 方式,如下:
client := &http.Client{
CheckRedirect: redirectPolicyFunc,
Timeout: time.Duration(10)*time.Second,//設置超時
}
client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyUrl)} //設置代理ip
resp, err := client.Get("http://example.com")
req, err := http.NewRequest("GET", "http://example.com", nil) //設置header
req.Header.Add("If-None-Match", `W/"wyzzy"`)
resp, err := client.Do(req)
這里官方文檔指出,client 只需要全局實例化,然后是協程安全的,所以,使用多協程的方式,用共享的client 去發送req 是可行的。
根據官方文檔,和我們的業務場景,我們寫出了如下的業務代碼:
var client *http.Client
//初始化全局client
func init (){
client = &http.Client{
Timeout: time.Duration(10)*time.Second,
}
}
type HttpClient struct {}
//提供給多協程調用
func (this *HttpClient) Fetch(dstUrl string, method string, proxyHost string, header map[string]string)(*http.Response){
//實例化req
req, _ := http.NewRequest(method, dstUrl, nil)
//添加header
for k, v := range header {
req.Header.Add(k, v)
}
//添加代理ip
proxy := "http://" + proxyHost
proxyUrl, _ := url.Parse(proxy)
client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyUrl)}
resp, err := client.Do(req)
return resp, err
}
當我們使用協程池并發開100個 worker 調用Fetch() 的時候,照理說,established 的連接應該是100個,但是,我壓測的時候,發現,established 的連接塊到一萬個了,net/http的連接池根本沒起作用?估計這是哪里用法不對吧。
使用python的庫并發請求是沒有任何問題的,那這個問題到底出在哪里?其實如果熟悉golang net/http庫的流程,就很清楚了,問題就處在上面的Transport ,每個transport 維護了一個連接池,我們代碼中每個協程都會new 一個transport ,這樣,就會不斷新建連接。
我們看下transport 的數據結構:
type Transport struct {
idleMu sync.Mutex
wantIdle bool // user has requested to close all idle conns
idleConn map[connectMethodKey][]*persistConn
idleConnCh map[connectMethodKey]chan *persistConn
reqMu sync.Mutex
reqCanceler map[*Request]func()
altMu sync.RWMutex
altProto map[string]RoundTripper // nil or map of URI scheme => RoundTripper
//Dial獲取一個tcp 連接,也就是net.Conn結構,
Dial func(network, addr string) (net.Conn, error)
}
結構體中兩個map, 保存的就是不同的協議 不同的host,到不同的請求 的映射。非常明顯,這個結構體應該是和client 一樣全局的。所以,為了避開使用連接池失效,是不能不斷new transport 的!
我們不斷new transport 的原因就是為了設置代理,這里不能使用這種方式了,那怎么達到目的?如果知道代理的原理,我們這里解決其實很簡單,請求使用ip ,host 帶上域名就ok了。代碼如下:
var client *http.Client
func init (){
client = &http.Client{}
}
type HttpClient struct {}
func NewHttpClient()(*HttpClient){
httpClient := HttpClient{}
return &httpClient
}
func (this *HttpClient) replaceUrl(srcUrl string, ip string)(string){
httpPrefix := "http://"
parsedUrl, err := url.Parse(srcUrl)
if err != nil {
return ""
}
return httpPrefix + ip + parsedUrl.Path
}
func (this *HttpClient) downLoadFile(resp *http.Response)(error){
//err write /dev/null: bad file descriptor#
out, err := os.OpenFile("/dev/null", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
defer out.Close()
_, err = io.Copy(out, resp.Body)
return err
}
func (this *HttpClient) Fetch(dstUrl string, method string, proxyHost string, header map[string]string, preload bool, timeout int64)(*http.Response, error){
// proxyHost 換掉 url 中請求
newUrl := this.replaceUrl(dstUrl, proxyHost)
req, _ := http.NewRequest(method, newUrl, nil)
for k, v := range header {
req.Header.Add(k, v)
}
client.Timeout = time.Duration(timeout)*time.Second
resp, err := client.Do(req)
return resp, err
}
使用header 中加host 的方式后,這里的tcp 建連數 立刻下降到和協程池數量一致,問題得到解決。
來自:https://studygolang.com/articles/12590#reply3