golang 高並發下 tcp 建連數暴漲的原因分析

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

    背景:服務需要高頻發出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)} //設定代理ipresp, 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//初始化全域clientfunc 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){    //執行個體化reqreq, _ := http.NewRequest(method, dstUrl, nil)    //添加headerfor k, v := range header {req.Header.Add(k, v)}    //添加代理ipproxy := "http://" + proxyHostproxyUrl, _ := 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.Clientfunc 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.Secondresp, err := client.Do(req)    return resp, err}

    使用header 中加host 的方式後,這裡的tcp 建連數 立刻下降到和協程池數量一致,問題得到解決。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.