httpclient 并发 导致 goroutine 泄露 报错 socket too many files

2019-12-23 15:02:03 +08:00
 tim0991

代码背景

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

问题

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 代码

7396 次点击
所在节点    Go 编程语言
48 条回复
index90
2019-12-23 16:17:39 +08:00
Google 一下 too many time wait 就知道啦,就是修改内核参数。

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

我会选择编写自己的 proxy 函数,每次返回一个 ip port,这样就可以只用一个 httpClient 和一个 httpTransport,就可以利用 MaxIdleConnsPerHost,控制打开的连接数。
yuzhiquan
2019-12-23 16:19:21 +08:00
open files 或者设置 tw_recycle
sagaxu
2019-12-23 16:23:53 +08:00
@index90 timewait 是不占用 fd 的
tim0991
2019-12-23 16:32:29 +08:00
@index90 你的意思就是不并发?在我理解中 ip 变化 transport 必须要重新实例化吧
index90
2019-12-23 16:45:17 +08:00
#23 说得对

@tim0991 #24 可以并发啊,Transport.Proxy 只是一个函数,每次请求都会调用。你对 scanner 封装成一个闭包函数就可以了。
tim0991
2019-12-23 17:01:46 +08:00
@index90 我有点笨 没想通。。。能不能给个代码示例看一下 我理解你说的和我现在的做法好像没区别 😢
jedihy
2019-12-23 17:06:10 +08:00
SO_LINGER 设置成 0。
darrh00
2019-12-23 17:18:19 +08:00
你把 ulimit -n 输出的结果作为 queueCh 的大小,有必要开这么大?
aliipay
2019-12-23 17:38:18 +08:00
@EthanDo 你給的文档是 get 接口的,楼主调用的 do, 不是一回事
monsterxx03
2019-12-23 17:46:23 +08:00
我知道为啥了, 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
monsterxx03
2019-12-23 17:47:10 +08:00
@monsterxx03 说法有点问题,不是一次 request, 是一个 transport 内部会有一个 event loop 用的 fd
icexin
2019-12-23 18:11:13 +08:00
你这个的问题是每个请求一个 client,导致打开链接太多导致的。我之前回复的一个问题或许能帮到你,只需要一个 http client 就行 https://www.v2ex.com/t/622953#r_8247009 https://gist.github.com/icexin/f3c77f17dcc28e5f43c8cdcc4e88e9da
index90
2019-12-23 18:15:45 +08:00
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,
}
index90
2019-12-23 18:18:08 +08:00
#32 的代码更好
aliipay
2019-12-23 19:02:19 +08:00
@monsterxx03 试了下 size/2-10 果然没问题了
monsterxx03
2019-12-23 19:10:26 +08:00
@aliipay 你还是应该试试上面说的复用 transport, 现在做法并不好
EthanDon
2019-12-23 20:53:02 +08:00
@aliipay。。。你仔细看下源码就会发现 get、post、下面都是 do
lincanbin
2019-12-23 22:25:50 +08:00
开启快速回收 TIME_WAIT
SunRunAway
2019-12-23 23:20:30 +08:00
一个 Transport 会默认维护一个容量为 2 的连接池,你每个请求开一个 Transport so....
gamexg
2019-12-24 00:20:43 +08:00
没看代码,只看了回复

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

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

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/631536

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX