Fork me on GitHub

版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | https://vearne.cc

本文基于Golang 1.17.6

1.前言

之前萌叔曾在文章
imroc/req 连接池使用须知 提及过Golang标准库net/http提供的连接池http.Transport,但是是浅尝辄止。
本文萌叔想从http.Transport出发来谈谈一个连接池设计应该考虑哪些问题?

2.连接池的功能特征

下图是针对Grpc和Thrift压测结果(见参考资料1)。我么可以看出,长连接相比与短连接相比,QPS大概提升了1倍多。这是因为长连接减少连接建立的所需的3次握手。

要对长连接进行管理,特别是对闲置的长连接进行管理,就不可避免的引入连接池。

特征

  • 需要一个连接时,并不一定真的创建新连接,而是优先尝试从连接池选出空闲连接;如果连接池对应的连接为空,才创建新连接。
  • 销毁并不是真的销毁,而是将使用完毕的连接放回连接池中(逻辑关闭)。

这里引出了几个问题。

  • Q1:获取连接阶段,我们有没有办法知道从连接池中取出的空闲连接(复用)是有效的,还是无效的?
  • Q2:把使用完毕的连接放回连接池的阶段,空闲连接数量是否要做上限的约束。如果空闲连接数量有上限约束且空闲连接的数量已经达到上限。那么把连接放回连接池的过程,必然需要将之前的某个空闲连接进行关闭,那么按照什么规则选择这个需要关闭的连接。
  • Q3:放置在连接池中的连接,随着时间的流逝,它可能会变成无效连接(stale)。比如由于Server端定时清理空闲连接。那么为了确保连接池中连接的有效性,是否需要引入定时的检查逻辑?

3.net/http中连接池的实现

net/http中连接池的实现代码在
net/http/transport.go

获取连接

Transport.RoundTrip() -> Transport.getConn()

放回连接(逻辑关闭)

Response.Body.Close() -> bodyEOFSignal.Close() -> Transport.tryPutIdleConn()

为了约束空闲连接的数量,连接池引入了几个变量:
MaxIdleConns MaxIdleConns controls the maximum number of idle (keep-alive) connections across all hosts.
所有host总的最大空闲连接数量,默认值是100
MaxIdleConnsPerHostif non-zero, controls the maximum idle
(keep-alive) connections to keep per-host。
针对每个Host能够保持的最大空闲连接数量。默认值是2
这个是一个比较有意思的变量,因为默认情况,所有的HTTP请求都使用同一个连接池,由于MaxIdleConns存在,如果针对某个Host的 连接占用了大量空间,那么针对其它Host的连接可能就没有存储空间了。
MaxConnsPerHost MaxConnsPerHost optionally limits the total number of connections per host, including connections in the dialing, active, and idle states.
默认值是0,表示不限制。

有意思的点

transport中的连接是按照key存储,key可以对应到下面的结构

type connectMethod struct {
    _            incomparable
    proxyURL     *url.URL // nil for no proxy, else full proxy URL
    targetScheme string   // "http" or "https"
    // If proxyURL specifies an http or https proxy, and targetScheme is http (not https),
    // then targetAddr is not included in the connect method key, because the socket can
    // be reused for different targetAddr values.
    targetAddr string
    onlyH1     bool // whether to disable HTTP/2 and force HTTP/1
}
connectMethod.key().String() Description
|http|foo.com https directly to server, no proxy
|https|foo.com https directly to server, no proxy
|https,h1|foo.com https directly to server w/o HTTP/2, no proxy
http://proxy.com|https|foo.com http to proxy, then CONNECT to foo.com
http://proxy.com|http http to proxy, http to anywhere after that
socks5://proxy.com|http|foo.com socks5 to proxy, then http to foo.com
socks5://proxy.com|https|foo.com socks5 to proxy, then https to foo.com
https://proxy.com|https|foo.com https to proxy, then CONNECT to foo.com
https://proxy.com|http https to proxy, http to anywhere after that

注意: 目标地址如果是同一个域名则算作同一个Host

Q2:把连接放回连接池的过程,必然需要将之前的某个空闲连接进行关闭,那么按照什么规则选择这个需要关闭的连接。

A2:

如果连接池已经满了(MaxIdleConns),那么放回空闲连接的同时,还需要从连接池中选出一个旧连接进行关闭。这个选择的规则依据LRU进行筛选。net/http使用的双向链表。

Q3: 为了确保连接池中连接的有效性,是否需要引入定时的检查逻辑?

A3:

net/http没有引入定期检查逻辑,但是额外的增加了一个变量
IdleConnTimeout: IdleConnTimeout is the maximum amount of time an idle (keep-alive) connection will remain idle before closing itself.
超过IdleConnTimeout的空闲连接将强制关闭。默认设置是90秒。

注意: 细心的读者可能发现,标准库使用的是time.Timer来执行对超过IdleConnTimeout的空闲连接的强制关闭。而没有使用时间轮,或是delayqueue。这是因为 1) time.timer使用更加直观 2) go 1.14之后 time.timer的性能已经足够好。

这个逻辑可能是:"过长时间的空闲连接都是不可信赖的"

一个从连接池中获得的连接只有在真正使用时,才能确定它是否有效。
如果连接在使用时报错,需要执行shouldRetryRequest()以确定是否需要获取新连接来执行失败的HTTP请求。

func (pc *persistConn) shouldRetryRequest(req *Request, err error) bool {
        ...
    if err == errServerClosedIdle {
        // The server replied with io.EOF while we were trying to
        // read the response. Probably an unfortunately keep-alive
        // timeout, just as the client was writing a request.
        return true
    }
    return false // conservatively
}

比如如果是errServerClosedIdle,服务端关闭了空闲连接导致请求失败,那么显然应该重新获取一个新连接,再发起一次请求。

4. 总结

看net/http的连接池,萌叔发现它与数据库的连接池、甚至与进程内的本地缓存在设计要点有很多相似之处。比如

  • 连接池大小的限制 <--> 缓存大小的限制
  • stale空闲连接的检查 <--> 过期key的清理
  • 达到空闲连接上限后,连接的换入换出 <--> 缓存满了之后,数据的换入换出

5. 参考资料

1.grpc和thrift性能对比
2.如何设计并实现一个db连接池?


微信公众号

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注