Go HTTP连接复用

HTTP 1.1中给出了连接复用的方法,即通过设定Header为Connection: keep-alive,服务端如果支持此选项,那么会在返回中同样设置该Header,请求结束后不会立即关闭连接。

HTTP的连接复用与TCP的reuse是两回事,两个使用不同的机制实现。

这里描述的“连接”,包括TCP连接,也包含其上的TLS连接,因此HTTP的Keep Alive实现的连接复用,省去了TCP连接建立以及TLS连接建立的过程。

服务端

// If this was an HTTP/1.0 request with keep-alive and we sent a
// Content-Length back, we can make this a keep-alive response ...
if w.wants10KeepAlive && keepAlivesEnabled {
	sentLength := header.get("Content-Length") != ""
	if sentLength && header.get("Connection") == "keep-alive" {
		w.closeAfterReply = false
	}
}

客户端

Golang的HTTP Client通过net/http/trasnport.go中的Transport对象实现底层TLS/TCP连接的封装。在Transport中,主要有以下几个参数:

在Dial建立连接后,就会开始进行读循环和写循环。在读循环中,能够获得HTTP Response,其中包括Header以及Body。当Body被读至末尾EOF,或者被手动关闭时,这个connection就被视为idle,可以回收用于其它请求了。

在发送请求后,如果Body里有东西,那么必须手动读取Body至EOF,并手动Close才能使其TCP连接得到复用。

net/http/transport.go中,这一段描述了这个逻辑:

		waitForBodyRead := make(chan bool, 2)
		body := &bodyEOFSignal{
			body: resp.Body,
			earlyCloseFn: func() error {
				waitForBodyRead <- false
				<-eofc // will be closed by deferred call at the end of the function
				return nil

			},
			fn: func(err error) error {
				isEOF := err == io.EOF
				waitForBodyRead <- isEOF
				if isEOF {
					<-eofc // see comment above eofc declaration
				} else if err != nil {
					if cerr := pc.canceled(); cerr != nil {
						return cerr
					}
				}
				return err
			},
		}

		/// ...
				// Before looping back to the top of this function and peeking on
		// the bufio.Reader, wait for the caller goroutine to finish
		// reading the response body. (or for cancellation or death)
		select {
		case bodyEOF := <-waitForBodyRead:
			pc.t.setReqCanceler(rc.cancelKey, nil) // before pc might return to idle pool
			alive = alive &&
				bodyEOF &&
				!pc.sawEOF &&
				pc.wroteRequest() &&
				tryPutIdleConn(trace)
			if bodyEOF {
				eofc <- struct{}{}
			}
		case <-rc.req.Cancel:
			alive = false
			pc.t.CancelRequest(rc.req)
		case <-rc.req.Context().Done():
			alive = false
			pc.t.cancelRequest(rc.cancelKey, rc.req.Context().Err())
		case <-pc.closech:
			alive = false
		}

在下面这一句中:

	alive = alive &&
		bodyEOF &&
		!pc.sawEOF &&
		pc.wroteRequest() &&
		tryPutIdleConn(trace)

只有符合以下条件的前提下,才能尝试将连接回收放入连接池:

  1. 开启KeepAlive
  2. Body读取到EOF
  3. TCP连接没有被关闭(pc.sawEOF)
  4. 请求已经彻底发送并成功

返回码非2XX不影响回收,因为这是业务层的成功失败,而非HTTP/TCP本身的成功或失败。

端口耗尽问题

当使用Go client不断请求主机连接,而又没有合适的设定MaxConnsPerHost时,TCP连接将会不断被创建,每创建一个TCP连接,就要用掉一个主机端口。当/etc/sysctl.conf中的net.ipv4.ip_local_port_range指定范围端口全部耗尽时,新建立连接将会报错:

connect: cannot assign requested address

解决该问题,应当妥善设置最大连接数限制,在应用程序中设置适当的并发数。