Golang Go语言中 httpclient 并发导致 goroutine 泄露 报错 socket too many files

Golang Go语言中 httpclient 并发导致 goroutine 泄露 报错 socket too many files

代码背景

使用 golang 验证代理 Ip,代码主要作用如下

  • 通过扫描然后扫描得到一个 ip 文件,每行一个代理 ip
  • 遍历文件按行读取 每行使用代理 ip 发起一个 http 请求 验证之后输出日志
  • client 数量通过 bufferd channel 控制 小于 ulimit -n

问题

ip 文件内容一般是 100W 行以上,程序运行一段时间之后会出现socket: too many files open

我的尝试

最开始以为是持久连接的问题,就设置了keep-alive: false,设置之后发现还是有问题 使用 pprof 调试发现很多 goroutine 卡在这里,但是此时 channel 长度是比设定值要小的,代表是可以接收数据,等于是老的 goroutine 没有释放,新的 goroutine 一直在创建

internal/poll.runtime_pollWait(0x7f004f1ca2f8, 0x72, 0xffffffffffffffff)
	/usr/local/go/src/runtime/netpoll.go:184 +0x55
internal/poll.(*pollDesc).wait(0xc0029e6f18, 0x72, 0x1000, 0x1000, 0xffffffffffffffff)
	/usr/local/go/src/internal/poll/fd_poll_runtime.go:87 +0x45
internal/poll.(*pollDesc).waitRead(...)
	/usr/local/go/src/internal/poll/fd_poll_runtime.go:92
internal/poll.(*FD).Read(0xc0029e6f00, 0xc002938000, 0x1000, 0x1000, 0x0, 0x0, 0x0)
	/usr/local/go/src/internal/poll/fd_unix.go:169 +0x1cf
net.(*netFD).Read(0xc0029e6f00, 0xc002938000, 0x1000, 0x1000, 0x0, 0x0, 0xc001f21f18)
	/usr/local/go/src/net/fd_unix.go:202 +0x4f
net.(*conn).Read(0xc0017ae198, 0xc002938000, 0x1000, 0x1000, 0x0, 0x0, 0x0)
	/usr/local/go/src/net/net.go:184 +0x68
bufio.(*Reader).fill(0xc00180ca20)
	/usr/local/go/src/bufio/bufio.go:100 +0x103
bufio.(*Reader).ReadSlice(0xc00180ca20, 0xa, 0xc001f21840, 0xc001f21888, 0x40c0c6, 0xc00087e120, 0x90)
	/usr/local/go/src/bufio/bufio.go:359 +0x3d
bufio.(*Reader).ReadLine(0xc00180ca20, 0x8, 0xc0006c6a80, 0x7f0051656460, 0x0, 0x2, 0xc329f8)
	/usr/local/go/src/bufio/bufio.go:388 +0x34
net/textproto.(*Reader).readLineSlice(0xc001f21960, 0xc00087e120, 0xc002938000, 0x7f004f3698c8, 0xc0027bdd01, 0x101000000950280)
	/usr/local/go/src/net/textproto/reader.go:57 +0x6c
net/textproto.(*Reader).ReadLine(...)
	/usr/local/go/src/net/textproto/reader.go:38
net/http.ReadResponse(0xc00180ca20, 0xc00106b400, 0x1000, 0xc002938000, 0xc0017ae198)
	/usr/local/go/src/net/http/response.go:161 +0xd1
net/http.(*Transport).dialConn(0xc002945a40, 0x94cd60, 0xc000024100, 0xc0029e6d80, 0x8b4508, 0x5, 0xc002685940, 0x11, 0x0, 0xc000288fa8, ...)
	/usr/local/go/src/net/http/transport.go:1544 +0x85a
net/http.(*Transport).dialConnFor(0xc002945a40, 0xc000ec1ce0)
	/usr/local/go/src/net/http/transport.go:1308 +0xdc
created by net/http.(*Transport).queueForDial
	/usr/local/go/src/net/http/transport.go:1277 +0x41d

因为阅读 golang http 源码太过于吃力,所以只大概跟了一下代码,我理解这段代码是创建 connection 请求并返回, 想请教一下各位这个 connection 不释放的 具体原因到底是为什么

代码和测试文件

测试文件 golang 代码


更多关于Golang Go语言中 httpclient 并发导致 goroutine 泄露 报错 socket too many files的实战教程也可以访问 https://www.itying.com/category-94-b0.html

49 回复

用一个全局的 http.Client 就行。不需要每次 new 个新的。

更多关于Golang Go语言中 httpclient 并发导致 goroutine 泄露 报错 socket too many files的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


文件跟数据库是两回事,你这并行验证的需求要用的不是文件数据,而是数据库

难道不是代理 ip 不同 transport 不同吗? transport 不同还能用同一个 client 吗?

谢谢指点 但就事论事 想了解一下 为什么以及如何解决文中的 goroutine 泄露问题

这个坑我踩过。。。

你看下你的 http 请求的 response 的 body 是不是没有关闭。这个 body 不管请求发送过程有没有出错,都要调用 body.Close()的。可以看下 go 的文档: https://golang.org/pkg/net/http/
“The client must close the response body when finished with it:”

还有个操作是给所有 http 请求加上超时时间。

https://stackoverflow.com/questions/37454236/net-http-server-too-many-open-files-error/48342086#48342086?newreg=e8bd30ac66d443138486653353d0c59a
https://sanyuesha.com/2019/09/10/go-http-request-goroutine-leak/

你好 谢谢你的回复 首先 body 我关了,其次我给 http.client 设置了超时, 最后 我在问题的结尾留了代码地址 如果你有空可以看看 帮忙指点一下的话感激不尽

这跟 goroutine 和 http 应该没有关系。
单纯是 TIME_WAIT 的连接太多了。

虽然没有调试代码,但是,起的 go 程数是 ulimit -n 的,会不会太多?可以把控制 go 程的代码 queueCh <- true 放到 go 程外面。

那请问 1. 如何控制 time wait 数量? 2 如何主动关闭 time wait?

7 楼说的也是一种可能,可以打开快速回收优化下。

谢谢 我生成环境 channel 是在 goroutine 外的,这个是临时准备用来测试的,然后 ulimit -n 我设置的是 10000W 然后 channel 长度就是 9000 这应该不算长吧,现在只有单个进程

试一下在关闭之前:
io.Copy(ioutil.Discard, resp.Body)

queueCh <- true 这行要放在 go func() 之前, 不然你希望的阻塞不会生效的.

还有你 wg.Add(1) 放在 continue 的判断之后, 不然假如有空行, 最后 Wait 就永远结束不了

还是有问题

感谢你的意见 代码是早上在地铁上面写的 有点匆忙不好意思,然后我按照你的建议改过之后 任然是同样的错误

ulimit -n 只对单次会话有效
持久化要设置 sysctl

而且 9000 并发是不是太高了,有这么大的带宽吗

问题只针对单次回话,带宽是另外的问题了,假设有吧。。。

在本机测试了一下,结论 TIMEWAIT 太多,TIMEWAIT 都会占用 fd

那请问应该怎么解决呢。。。。。我用 time wait 关键字搜索了一下 都说加 disable keep alive 就好了。。。。 能不能麻烦指点一下方向

Google 一下 too many time wait"

Google 一下 too many time wait 就知道啦,就是修改内核参数。

但是感觉这个不是正确的思路。

我会选择编写自己的 proxy 函数,每次返回一个 ip port,这样就可以只用一个 httpClient 和一个 httpTransport,就可以利用 MaxIdleConnsPerHost,控制打开的连接数。

open files 或者设置 tw_recycle

timewait 是不占用 fd 的

你的意思就是不并发?在我理解中 ip 变化 transport 必须要重新实例化吧

#23 说得对

#24 可以并发啊,Transport.Proxy 只是一个函数,每次请求都会调用。你对 scanner 封装成一个闭包函数就可以了。

我有点笨 没想通。。。能不能给个代码示例看一下 我理解你说的和我现在的做法好像没区别 😢

SO_LINGER 设置成 0。

你把 ulimit -n 输出的结果作为 queueCh 的大小,有必要开这么大?

你給的文档是 get 接口的,楼主调用的 do, 不是一回事

我知道为啥了, go 的 http client 一次 request 底下会开两个 fd, 一个是 tcp connection, 还有一个是它内部 net poller 用来做 eventloop 的, 所以你用 ulimit -40 做 size 还是会挂的

你试试把 size /2 作为 channel 的 buffer size 试试.

不过楼主你这代码有个更大的问题, ip, port 要显示传递给 go func(), 不然在一个 for loop 里启动的 goroutine 执行时候拿到的不一定是你想的那个 ip, port

说法有点问题,不是一次 request, 是一个 transport 内部会有一个 event loop 用的 fd

你这个的问题是每个请求一个 client,导致打开链接太多导致的。我之前回复的一个问题或许能帮到你,只需要一个 http client 就行 https://www.v2ex.com/t/622953#r_8247009 https://gist.github.com/icexin/f3c77f17dcc28e5f43c8cdcc4e88e9da

transport := &http.Transport{
Proxy: func(request *http.Request) (u *url.URL, err error) {
host, ok := <-scannerChan
if !ok {
return nil, errors.New(“scanner channel closed”)
}
return &url.URL{Host: fmt.Sprintf("%s:%s", ip, port)}, nil
},
//Proxy: http.ProxyURL(&url.URL{Host: fmt.Sprintf("%s:%s", ip, port)}),
DialContext: (&net.Dialer{
KeepAlive: -1,
}).DialContext,
DisableKeepAlives: true,
MaxIdleConns: 1000,
MaxIdleConnsPerHost: -1,
MaxConnsPerHost: 0,
IdleConnTimeout: 0,
DisableCompression: true,
}

#32 的代码更好

试了下 size/2-10 果然没问题了

你还是应该试试上面说的复用 transport, 现在做法并不好

。。。你仔细看下源码就会发现 get、post、下面都是 do

开启快速回收 TIME_WAIT

一个 Transport 会默认维护一个容量为 2 的连接池,你每个请求开一个 Transport so…

没看代码,只看了回复

开了很多 httpclient ?
httpclient 内部有连接池,如果不断开新的 http。client,建议去调用下 CloseIdleConnections 函数。

另外如果还是出问题,那么建议直接自己管理连接。 req.Write 和 WriteProxy 函数。

感谢你的回复 尝试了你的代码之后暂时没发现报错了,但是有个疑问,transport 内部管理的是 tcp conn,同一个 client 和 transport 可以复用不同的 host 的 conn 吗?

#41 transport 内部有连接池

话说我在 A 城市网络,即使 size 减半也遇到 too many open file,换到 B 城市网络,即使不减半也不会有问题。

同问

感谢你们的耐心解答 我昨天尝试了使用 size/2 然后同一个 client 和 transport 之后 ulimit 调整到 2W 尝试跑了 8000W 数据没出现问题。

内部连接池的问题 我也有疑问 我理解是 host 不通 连接池不复用,既然不复用的话 那为什么之前的问题就好了
同时附上我昨天修改之后的代码 https://goplay.space/#L1HS0igSwwc

不知道 MaxIdleConnsPerHost 在 tcp conn 复用中起到怎么样的作用,我看代码发现其作用是用来控制 transport.tryPutIdleConn 方法中是否把 conn 加入连接池,所以我把 MaxIdleConnsPerHost 关了 但是这样的话不就不能复用了吗?
那这样使用同一个 transport 和 client 意义何在?

所以,仔细想想, 为什么 get/post 需要无条件 close,do 却不用。

实际情况中我也用的是 do,而且我也是无条件 close 的


func Get(url string) (resp *Response, err error) {
return DefaultClient.Get(url)
}
func (c *Client) Get(url string) (resp *Response, err error) {
req, err := NewRequest(“GET”, url, nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
func (c *Client) Do(req *Request) (*Response, error) {
return c.do(req)
}
有什么区别吗。。。


看了下文档,get 方法也是判断是否 err 后再 close 的,和 do 一样。 之前说法是有问题。
在 do/get 返回 error 时候,resp 是个 nil,不能调用 body.close 的。

在Golang中,当使用 http.Client 进行并发请求时,如果遇到 “socket: too many open files” 错误,这通常是由于系统级别的文件描述符限制被突破,而文件描述符在Go中也被用于网络连接(包括HTTP连接)。这种情况往往与goroutine泄露有关,即goroutine被错误地创建后未能正确关闭。

解决此问题的步骤如下:

  1. 检查HTTP客户端的复用:确保你的 http.Client 实例是复用的,而不是为每个请求都创建一个新的实例。复用 http.Client 可以有效利用连接池,减少不必要的连接开销。

  2. 设置合理的超时:为 http.ClientTimeoutTransportIdleConnTimeout 设置合理的值,以确保空闲连接能够被及时关闭,避免资源泄露。

  3. 监控和关闭goroutine:确保所有启动的goroutine都有明确的退出路径。使用 sync.WaitGroupcontext.Context 来管理goroutine的生命周期,确保它们在完成任务后被正确关闭。

  4. 增加文件描述符限制:虽然这不是解决问题的根本方法,但可以通过调整系统的文件描述符限制(如Linux中的 ulimit -n)来临时缓解问题。注意,这只是权宜之计,真正解决还需从代码层面入手。

  5. 使用工具检测泄露:利用如 pprofrace 检测器等工具,帮助你定位和修复goroutine泄露的问题。

通过上述步骤,你应该能够有效地解决因并发导致的goroutine泄露及文件描述符耗尽的问题。

回到顶部