使用 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 不释放的 具体原因到底是为什么
1
guonaihong 2019-12-23 15:08:40 +08:00
用一个全局的 http.Client 就行。不需要每次 new 个新的。
|
2
opengps 2019-12-23 15:09:12 +08:00
文件跟数据库是两回事,你这并行验证的需求要用的不是文件数据,而是数据库
|
3
tim0991 OP @guonaihong 难道不是代理 ip 不同 transport 不同吗? transport 不同还能用同一个 client 吗?
|
5
EthanDon 2019-12-23 15:14:13 +08:00
这个坑我踩过。。。
你看下你的 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/ |
6
tim0991 OP @EthanDon 你好 谢谢你的回复 首先 body 我关了,其次我给 http.client 设置了超时, 最后 我在问题的结尾留了代码地址 如果你有空可以看看 帮忙指点一下的话感激不尽
|
7
lishunan246 2019-12-23 15:25:34 +08:00
这跟 goroutine 和 http 应该没有关系。
单纯是 TIME_WAIT 的连接太多了。 |
8
guonaihong 2019-12-23 15:26:42 +08:00
虽然没有调试代码,但是,起的 go 程数是 ulimit -n 的,会不会太多?可以把控制 go 程的代码 queueCh <- true 放到 go 程外面。
|
9
tim0991 OP @lishunan246 那请问 1. 如何控制 time wait 数量? 2 如何主动关闭 time wait?
|
10
guonaihong 2019-12-23 15:28:52 +08:00
7 楼说的也是一种可能,可以打开快速回收优化下。
|
11
tim0991 OP @guonaihong 谢谢 我生成环境 channel 是在 goroutine 外的,这个是临时准备用来测试的,然后 ulimit -n 我设置的是 10000W 然后 channel 长度就是 9000 这应该不算长吧,现在只有单个进程
|
12
index90 2019-12-23 15:31:53 +08:00
试一下在关闭之前:
io.Copy(ioutil.Discard, resp.Body) |
13
monsterxx03 2019-12-23 15:34:11 +08:00
queueCh <- true 这行要放在 go func() 之前, 不然你希望的阻塞不会生效的.
还有你 wg.Add(1) 放在 continue 的判断之后, 不然假如有空行, 最后 Wait 就永远结束不了 |
15
tim0991 OP @monsterxx03 感谢你的意见 代码是早上在地铁上面写的 有点匆忙不好意思,然后我按照你的建议改过之后 任然是同样的错误
|
16
rimutuyuan 2019-12-23 15:45:04 +08:00
ulimit -n 只对单次会话有效
持久化要设置 sysctl 而且 9000 并发是不是太高了,有这么大的带宽吗 |
17
tim0991 OP @rimutuyuan 问题只针对单次回话,带宽是另外的问题了,假设有吧。。。
|
18
index90 2019-12-23 15:56:37 +08:00
在本机测试了一下,结论 TIMEWAIT 太多,TIMEWAIT 都会占用 fd
|
19
tim0991 OP @index90 那请问应该怎么解决呢。。。。。我用 time wait 关键字搜索了一下 都说加 disable keep alive 就好了。。。。 能不能麻烦指点一下方向
|
20
index90 2019-12-23 16:14:36 +08:00
Google 一下 too many time wait"
|
21
index90 2019-12-23 16:17:39 +08:00
Google 一下 too many time wait 就知道啦,就是修改内核参数。
但是感觉这个不是正确的思路。 我会选择编写自己的 proxy 函数,每次返回一个 ip port,这样就可以只用一个 httpClient 和一个 httpTransport,就可以利用 MaxIdleConnsPerHost,控制打开的连接数。 |
22
yuzhiquan 2019-12-23 16:19:21 +08:00
open files 或者设置 tw_recycle
|
25
index90 2019-12-23 16:45:17 +08:00
|
27
jedihy 2019-12-23 17:06:10 +08:00 via iPhone
SO_LINGER 设置成 0。
|
28
darrh00 2019-12-23 17:18:19 +08:00
你把 ulimit -n 输出的结果作为 queueCh 的大小,有必要开这么大?
|
30
monsterxx03 2019-12-23 17:46:23 +08:00 1
我知道为啥了, 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 |
31
monsterxx03 2019-12-23 17:47:10 +08:00
@monsterxx03 说法有点问题,不是一次 request, 是一个 transport 内部会有一个 event loop 用的 fd
|
32
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
|
33
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, } |
34
index90 2019-12-23 18:18:08 +08:00
#32 的代码更好
|
35
aliipay 2019-12-23 19:02:19 +08:00
@monsterxx03 试了下 size/2-10 果然没问题了
|
36
monsterxx03 2019-12-23 19:10:26 +08:00 via iPhone
@aliipay 你还是应该试试上面说的复用 transport, 现在做法并不好
|
38
lincanbin 2019-12-23 22:25:50 +08:00
开启快速回收 TIME_WAIT
|
39
SunRunAway 2019-12-23 23:20:30 +08:00 via iPhone
一个 Transport 会默认维护一个容量为 2 的连接池,你每个请求开一个 Transport so....
|
40
gamexg 2019-12-24 00:20:43 +08:00
没看代码,只看了回复
开了很多 httpclient ? httpclient 内部有连接池,如果不断开新的 http。client,建议去调用下 CloseIdleConnections 函数。 另外如果还是出问题,那么建议直接自己管理连接。 req.Write 和 WriteProxy 函数。 |
41
tim0991 OP @icexin 感谢你的回复 尝试了你的代码之后暂时没发现报错了,但是有个疑问,transport 内部管理的是 tcp conn,同一个 client 和 transport 可以复用不同的 host 的 conn 吗?
|
42
index90 2019-12-24 09:47:53 +08:00
|
43
tim0991 OP @SunRunAway 同问
|
44
tim0991 OP @index90 @icexin 感谢你们的耐心解答 我昨天尝试了使用 size/2 然后同一个 client 和 transport 之后 ulimit 调整到 2W 尝试跑了 8000W 数据没出现问题。
内部连接池的问题 我也有疑问 我理解是 host 不通 连接池不复用,既然不复用的话 那为什么之前的问题就好了 同时附上我昨天修改之后的代码 https://goplay.space/#L1HS0igSwwc 不知道 MaxIdleConnsPerHost 在 tcp conn 复用中起到怎么样的作用,我看代码发现其作用是用来控制 transport.tryPutIdleConn 方法中是否把 conn 加入连接池,所以我把 MaxIdleConnsPerHost 关了 但是这样的话不就不能复用了吗? 那这样使用同一个 transport 和 client 意义何在? |
47
EthanDon 2019-12-24 12:05:28 +08:00
@aliipay
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) } 有什么区别吗。。。 |