背景

发压工具一般都需要提供尽量大的发压能力,发压工具在从Python迁移到Golang的过程中遇到了两个HttpClient连接相关的问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func NewHTTPClient() *http.Client {
	t := &http.Transport{
		DialContext: (&net.Dialer{
			Timeout:   30 * time.Second,
			KeepAlive: 30 * time.Second,
		}).DialContext,
		IdleConnTimeout:       180 * time.Second,
		TLSHandshakeTimeout:   10 * time.Second,
		ExpectContinueTimeout: 1 * time.Second,
		MaxIdleConns:          800,
		MaxIdleConnsPerHost:   800,
	}
	return &http.Client{
		Transport: t,
		Timeout:   time.Second * 10,
	}
}

func callEndpoint() bool {
	client := NewHTTPClient()
	httpReq, _ := http.NewRequest("GET", "https://google.com", nil)
	res, _ := client.Do(httpReq)
	defer func() {
		_ = res.Body.Close()
	}()
	if res.StatusCode == 200 {
		return true
	}
	return false
}
  • Q1,上述代码为什么连接没有复用,而是频繁创建?
  • Q2,连接池是如何复用的?

数据结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
type Client struct {
    // 具体数据传输实现
	Transport RoundTripper

    // 在重定向前执行,可以用来返回错误终止重定向或者修改Header等
	CheckRedirect func(req *Request, via []*Request) error

    // 相当于浏览器里每个域下的cookie管理
	Jar CookieJar

    // 请求超时时间
	Timeout time.Duration
}

type RoundTripper interface {
	RoundTrip(*Request) (*Response, error)
}

// Transport 是一个RoundTripper的实现
type Transport struct {
	idleMu       sync.Mutex
	idleConn     map[connectMethodKey][]*persistConn // most recently used at end
	idleConnWait map[connectMethodKey]wantConnQueue  // waiting getConns
	idleLRU      connLRU

	connsPerHostMu   sync.Mutex
	connsPerHost     map[connectMethodKey]int
	connsPerHostWait map[connectMethodKey]wantConnQueue // waiting getConns

    // 支持http、https、socks5代理
	Proxy func(*Request) (*url.URL, error)

    // 禁用长连接
	DisableKeepAlives bool

    // 最大空闲连接数,0代表不限制
	MaxIdleConns int

    // 单一Host的最大空闲连接数,0的话值为2
	MaxIdleConnsPerHost int

    // 单一Host的最大连接数,包含连接中、使用中、空闲状态下的,0代表不限制。
	MaxConnsPerHost int

    // 长连接能Idle多久才关闭,0代表不限制
	IdleConnTimeout time.Duration
}

type persistConn struct {
	t         *Transport
	cacheKey  connectMethodKey
	conn      net.Conn
	br        *bufio.Reader       // from conn
	bw        *bufio.Writer       // to conn
	nwrite    int64               // bytes written
	reqch     chan requestAndChan // written by roundTrip; read by readLoop
	writech   chan writeRequest   // written by roundTrip; read by writeLoop
	closech   chan struct{}       // closed when conn closed
	isProxy   bool
	sawEOF    bool  // whether we've seen EOF from conn; owned by readLoop
	readLimit int64 // bytes allowed to be read; owned by readLoop
    // 如果写入失败,则连接无法进行复用
	writeErrCh chan error

	// Both guarded by Transport.idleMu:
	idleAt    time.Time   // time it last become idle
	idleTimer *time.Timer // holding an AfterFunc to close it

	mu                   sync.Mutex // guards following fields
	closed               error // set non-nil when conn is closed, before closech is closed
	canceledErr          error // set non-nil if conn is canceled
	broken               bool  // an error has happened on this connection; marked broken so it's not reused.
	reused               bool  // whether conn has had successful request/response and is being reused.
}

源码分析

-> oneNote

如何设置连接池参数

核心参数就是MaxIdleConns、MaxIdleConnsPerHost、MaxConnsPerHost和IdleConnTimeout。 如果不使用连接池,短连接不断地建立和关闭,会导致产生非常多的TIME_WAIT,最后本地端口用光服务就不可用了。 如果只有一个Host,那MaxIdleConnsPerHost=MaxIdleConns,就只需要考虑最大连接数和最大空闲连接数了,具体还是看业务产生多大的并发调用。 比如上面代码,如果不设置MaxConnsPerHost,当并发数远超连接池大小时,依然会创建很多的连接,最后就会因没法放回连接池而频繁关闭。 这里也需要考虑闲时和忙时,如果MaxIdleConnsPerHost很大,忙时自然有优势,但是等到低峰期会存在大量空闲连接,所以这时候就很需要IdleConnTimeout,让连接自动关闭。

基本上database/sql连接池也类似,可参考 database/sql连接池源码分享

其他用法

ClientTrace

http库也提供了ClientTrace来方便定位问题,ClientTrace是一堆hook方法的集合,比如可以用来分析DNS解析画了多少时间,或者通过PutIdleConn方法知道是否连接复用上有error发生

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type ClientTrace struct {
	GetConn func(hostPort string)

	GotConn func(GotConnInfo)

	PutIdleConn func(err error)

	GotFirstResponseByte func()

	Got100Continue func()

	Got1xxResponse func(code int, header textproto.MIMEHeader) error

	DNSStart func(DNSStartInfo)

	DNSDone func(DNSDoneInfo)

	ConnectStart func(network, addr string)

	ConnectDone func(network, addr string, err error)

	TLSHandshakeStart func()

	TLSHandshakeDone func(tls.ConnectionState, error)

	WroteHeaderField func(key string, value []string)

	WroteHeaders func()

	Wait100Continue func()

	WroteRequest func(WroteRequestInfo)
}

Cookie管理

如果是请求前登陆,然后后续不断地请求,就可以不用单独给每个request设置单独的cookie了,和浏览器的使用一样

代理设置

可以支持http、https、socks5代理