携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情
前言
最近在搞go的项目,发现项目没有使用到http client连接池,让笔者对连接池的必要性、连接池的配置、连接的资损产生了一些思考,除此之外还会给出go http client连接池的示例。
为啥需要连接池
这个和线程池优点类似,主要是创建、销毁会消耗资源和时间,连接还没到应用层所以是tcp的创建和销毁。
tcp创建需要经过三次握手,销毁需要经过四次挥手。消耗的资源主要包括CPU、内存(包括各种数据结构及分配的缓冲区),此外还包括fd、端口号等。
因为DMA的存在,所以对cpu的消耗会比较少,但是具体指令需要CPU告诉DMA控制器,比如传输什么数据、从哪里传输到哪里。此外CPU还会参与TCP头部的解析。
内存可以使用sysctl -A|grep tcp_.mem
查看下:
net.ipv4.tcp_rmem = 8192 87380 16777216
net.ipv4.tcp_wmem = 8192 65536 16777216
复制代码
当然需要避免创建和销毁是针对同一个host的,所以连接池是每个host的连接池。
所以使用连接池能避免和同一个host频繁建立和销毁连接。同时也能限制最大连接数,但是服务一般不去限制,因为会导致进来的请求因为没有连接响应时间变长,需要做的是最外层的限流和扩容,做的话可以控制下游/第三方服务的压力,但是下游或者第三方自己应该需要做限流,不需要把控制耦合在上游。
什么时候需要使用httpclient连接池
如果每次请求的host都不一样就没必要用,但是绝大部分服务对外发起的请求都是已知的,不存在随机或者每次都不一样。如果是这样不用管是否请求量,加上肯定比不加好。
不过需要知道的是连接是双向的,发起方维护了连接,接收方也会维护,接收方会因为发起方的维护而占用资源,但是笔者认为接收方如果是第三方的时候肯定会考虑到这点,如果是自己的下游服务也需要做好限流和扩容,如果下游服务接收量有限,发起方服务需要做好限流而不是连接池的问题。
创建连接池
var (
httpClient *http.Client
)
func init() {
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
//InsecureSkipVerify用来控制客户端是否证书和服务器主机名。如果设置为true, 则不会校验证书以及证书中的主机名和服务器主机名是否一致。
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
DialContext: (&net.Dialer{
Timeout: 10 * time.Second, //拨号等待连接完成的最大时间
KeepAlive: 30 * time.Second, //保持网络连接活跃keep-alive探测间隔时间。
}).DialContext,
MaxIdleConns: 200,
IdleConnTimeout: 300 * time.Second,
MaxIdleConnsPerHost: 200,
}
httpClient = &http.Client{
Transport: tr,
Timeout: 30 * time.Second, //设置超时,包含connection时间、任意重定向时间、读取response body时间
}
}
复制代码
resp, err := httpClient.Do(req)
复制代码
- Dialer.Timeout:就是等待Connection的时间,这个网络正常情况下毫秒级别的,为了考虑异常情况设置大点,笔者这里设置了10s。
- Dialer.KeepAlive:当不传输的时候保持心跳的间隔。
- Transport.MaxIdleConns:允许的最大空闲连接,可以根据并发平均值去设置,不是越多越好。
- Transport.IdleConnTimeout:空闲连接超时时间,超过了之后回收,最好根据生产流量去设置,不会让空闲的长期占用资源,又不至于刚销毁流量就来了。
- Transport.MaxIdleConnsPerHost:如果只有一个下游服务调用就和MaxIdleConns设置成一样。
- Client.Timeout:包含connection时间、任意重定向时间、读取response body时间,肯定要大于Dialer.Timeout。
注意:最好依据压测和生成真实流量配置。