golang net/http httpclient两个只管生不管埋的坑位

http是我们最常见的客户端/服务端传输协议,在golang中,默认的net/http包有一些坑位,需要调整以获得更加性能。

在golang程序中,我也遇到因为不合理使用 http client导致的程序崩溃问题。


坑:1:默认的HttpClient不包含请求超时时间

如果你使用http.Get(url)或者&Client{}, 这将会使用http.DefaultClient,这个结构体内no timeout

假如发出请求的API有问题:没有及时响应httpclient请求但是hold了连接, 在高并发情况下,打开的连接数会持续增长,最终导致客户端服务器资源到达瓶颈。

解决方案:不要使用默认的HTTPClient, 总是为HttpClient指定Timeout

	client := &http.Client{
		Timeout: 10 * time.Second,
	}

HttpClient Timeout包括连接、重定向(如果有)、从Response Body读取的时间,内置定时器会在Get,Head、Post、Do 方法之后继续运行,直到读取完Response.Body。

这里有个有关HttpClient Timeout的排障问题,你可参考。


.NET HttpClient Timeout: The default value is 100,000 milliseconds (100 seconds).

坑2:默认的Http Transport连接池单主机可复用连接数只有2个

目前常见的HttpClient(.NET Core,golang) 都会有连接池的概念, 客户端会尽量复用池中已经建立的tcp连接 (sqlclient连接池也是复用的tcp连接)。

之前我有个误区,认为连接池是预置连接(因为有个开源作者实现的redis库是预置连接),其实不是的,连接池强调的是复用已创建的连接,连接池的创建是由首次请求来驱动的。

golang的Transport用于连接池化。

DefaultTransport is the default implementation of Transport and is
used by DefaultClient. It establishes network connections as needed
and caches them for reuse by subsequent calls. It uses HTTP proxies
as directed by the $HTTP_PROXY and $NO_PROXY (or $http_proxy and
$no_proxy) environment variables.

MaxIdleConns  int
MaxIdleConnsPerHost int    //  如果是0, 使用DefaultMaxIdleConnsPerHost=2

MaxConnsPerHost int         // 0意味无限制
IdleConnTimeout time.Duration  // 0 意味无限制

默认值:
MaxIdleConns =100, MaxIdleConnsPerHost=2
没有定义MaxConns字段,golang其实创建conn是无限制的,MaxConnsPerHost=0

var DefaultTransport RoundTripper = &Transport{
	Proxy: ProxyFromEnvironment,
	DialContext: (&net.Dialer{
		Timeout:   30 * time.Second,
		KeepAlive: 30 * time.Second,
	}).DialContext,
	ForceAttemptHTTP2:     true,
	MaxIdleConns:          100,
	IdleConnTimeout:       90 * time.Second,     // 空闲(keep-alive)连接在关闭之前保持空闲的时长
	TLSHandshakeTimeout:   10 * time.Second,
	ExpectContinueTimeout: 1 * time.Second,
}

http连接池化 ,是公共连接池, 能创建的连接是无限制的(虽然没字段,但是代码分析是无限制的), 每个Host能创建的连接MaxConnsPerHost=0 , 也是无限制的;

有坑位的是DefaultMaxIdleConnsPerHost=2:字面含义是连接池中每个主机的空闲连接数是2个,其实也就是每个主机能复用的连接数就是2个。


发现问题了吗? 能无限制创建,但是能复用的只有2个。

这意味着:如果你的请求是持续高并发请求, 一开始请求能无限制创建, 但是由于不能复用tcp连接(2个,聊胜于无), 造成客户端主动关闭tcp连接,time_wait状态(2min)会占用大量端口, 之后就不能发起tcp连接了。
有些同学不知道威力,我画个图理解一下。

在并发持续请求host的情况下, 因为不能复用tcp连接,就会频繁销毁连接, 这样会累积很多time_wait状态的不可用连接, 没过多久就创建不了了。

这个问题的本质是 主动关闭的TCP连接,服务器会产生大量的time-wait状态连接(2min),占用了可用的网络文件描述符。

解决方案:不要使用默认Transport,增加MaxIdleConnsPerHost


--- 本人回顾了.NET HttpClient,不用刻意关注这个值。

实际上,.NET也存在这个MaxIdleConnectionPerServer配置,但是.NET Core这个PerServer被设置为int.maxvalue,所以我们无需关注,.NET真香。


我的收获

通过本文,我们谈到了golang HttpClient的2个坑位、由坑位导致的现象和排障思路,各位看官,有则改之无则加勉。

坑1是不限制请求超时,在高并发的客户端,很大可能会形成雪崩。

其中坑2的池化机制(tcp连接复用)很有意思,结合tcp原理,我们可以认定MaxIdleConnsPerHost=2 基本就告别了连接复用

posted @ 2022-03-02 14:09  博客猿马甲哥  阅读(2884)  评论(0编辑  收藏  举报