golang 高并发下 tcp 建连数暴涨的原因分析

Golang   2018-03-16 11:06:30 发布
您的评价:
     
3.0
收藏     0收藏
文件夹
标签
(多个标签用逗号分隔)

 背景:服务需要高频发出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

扩展阅读

微博基于Docker容器的混合云迁移实战
Python 中的进程、线程、协程、同步、异步、回调
Golang编程经验总结
码农周刊分类整理
小米抢购限流峰值系统「大秒」架构解密

为您推荐

libcurl的封装,支持同步异步请求,支持多线程下载,支持https
Golang标准库探秘(二):快速搭建HTTP服务器
Golang测试技术
Ruby 2.1 详情
PHP生成二维码的类及方法

更多

Golang
Google Go/Golang开发
相关文档  — 更多
相关经验  — 更多
相关讨论  — 更多