The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
tim0991

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

  •  
  •   tim0991 · Dec 23, 2019 · 8415 views
    This topic created in 2333 days ago, the information mentioned may be changed or developed.

    代码背景

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

    48 replies    2019-12-24 13:01:43 +08:00
    guonaihong
        1
    guonaihong  
       Dec 23, 2019
    用一个全局的 http.Client 就行。不需要每次 new 个新的。
    opengps
        2
    opengps  
       Dec 23, 2019
    文件跟数据库是两回事,你这并行验证的需求要用的不是文件数据,而是数据库
    tim0991
        3
    tim0991  
    OP
       Dec 23, 2019
    @guonaihong 难道不是代理 ip 不同 transport 不同吗? transport 不同还能用同一个 client 吗?
    tim0991
        4
    tim0991  
    OP
       Dec 23, 2019
    @opengps 谢谢指点 但就事论事 想了解一下 为什么以及如何解决文中的 goroutine 泄露问题
    EthanDon
        5
    EthanDon  
       Dec 23, 2019
    这个坑我踩过。。。

    你看下你的 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/
    tim0991
        6
    tim0991  
    OP
       Dec 23, 2019
    @EthanDon 你好 谢谢你的回复 首先 body 我关了,其次我给 http.client 设置了超时, 最后 我在问题的结尾留了代码地址 如果你有空可以看看 帮忙指点一下的话感激不尽
    lishunan246
        7
    lishunan246  
       Dec 23, 2019
    这跟 goroutine 和 http 应该没有关系。
    单纯是 TIME_WAIT 的连接太多了。
    guonaihong
        8
    guonaihong  
       Dec 23, 2019
    虽然没有调试代码,但是,起的 go 程数是 ulimit -n 的,会不会太多?可以把控制 go 程的代码 queueCh <- true 放到 go 程外面。
    tim0991
        9
    tim0991  
    OP
       Dec 23, 2019
    @lishunan246 那请问 1. 如何控制 time wait 数量? 2 如何主动关闭 time wait?
    guonaihong
        10
    guonaihong  
       Dec 23, 2019
    7 楼说的也是一种可能,可以打开快速回收优化下。
    tim0991
        11
    tim0991  
    OP
       Dec 23, 2019
    @guonaihong 谢谢 我生成环境 channel 是在 goroutine 外的,这个是临时准备用来测试的,然后 ulimit -n 我设置的是 10000W 然后 channel 长度就是 9000 这应该不算长吧,现在只有单个进程
    index90
        12
    index90  
       Dec 23, 2019
    试一下在关闭之前:
    io.Copy(ioutil.Discard, resp.Body)
    monsterxx03
        13
    monsterxx03  
       Dec 23, 2019
    queueCh <- true 这行要放在 go func() 之前, 不然你希望的阻塞不会生效的.

    还有你 wg.Add(1) 放在 continue 的判断之后, 不然假如有空行, 最后 Wait 就永远结束不了
    tim0991
        14
    tim0991  
    OP
       Dec 23, 2019
    @index90 还是有问题
    tim0991
        15
    tim0991  
    OP
       Dec 23, 2019
    @monsterxx03 感谢你的意见 代码是早上在地铁上面写的 有点匆忙不好意思,然后我按照你的建议改过之后 任然是同样的错误
    14v45mJPBYJW8dT7
        16
    14v45mJPBYJW8dT7  
       Dec 23, 2019
    ulimit -n 只对单次会话有效
    持久化要设置 sysctl

    而且 9000 并发是不是太高了,有这么大的带宽吗
    tim0991
        17
    tim0991  
    OP
       Dec 23, 2019
    @rimutuyuan 问题只针对单次回话,带宽是另外的问题了,假设有吧。。。
    index90
        18
    index90  
       Dec 23, 2019
    在本机测试了一下,结论 TIMEWAIT 太多,TIMEWAIT 都会占用 fd
    tim0991
        19
    tim0991  
    OP
       Dec 23, 2019
    @index90 那请问应该怎么解决呢。。。。。我用 time wait 关键字搜索了一下 都说加 disable keep alive 就好了。。。。 能不能麻烦指点一下方向
    index90
        20
    index90  
       Dec 23, 2019
    Google 一下 too many time wait"
    index90
        21
    index90  
       Dec 23, 2019
    Google 一下 too many time wait 就知道啦,就是修改内核参数。

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

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

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

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

    另外如果还是出问题,那么建议直接自己管理连接。 req.Write 和 WriteProxy 函数。
    tim0991
        41
    tim0991  
    OP
       Dec 24, 2019
    @icexin 感谢你的回复 尝试了你的代码之后暂时没发现报错了,但是有个疑问,transport 内部管理的是 tcp conn,同一个 client 和 transport 可以复用不同的 host 的 conn 吗?
    index90
        42
    index90  
       Dec 24, 2019
    @tim0991 #41 transport 内部有连接池

    话说我在 A 城市网络,即使 size 减半也遇到 too many open file,换到 B 城市网络,即使不减半也不会有问题。
    tim0991
        43
    tim0991  
    OP
       Dec 24, 2019
    @SunRunAway 同问
    tim0991
        44
    tim0991  
    OP
       Dec 24, 2019
    @index90 @icexin 感谢你们的耐心解答 我昨天尝试了使用 size/2 然后同一个 client 和 transport 之后 ulimit 调整到 2W 尝试跑了 8000W 数据没出现问题。

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

    不知道 MaxIdleConnsPerHost 在 tcp conn 复用中起到怎么样的作用,我看代码发现其作用是用来控制 transport.tryPutIdleConn 方法中是否把 conn 加入连接池,所以我把 MaxIdleConnsPerHost 关了 但是这样的话不就不能复用了吗?
    那这样使用同一个 transport 和 client 意义何在?
    aliipay
        45
    aliipay  
       Dec 24, 2019
    @EthanDon 所以,仔细想想, 为什么 get/post 需要无条件 close,do 却不用。
    EthanDon
        46
    EthanDon  
       Dec 24, 2019
    @aliipay 实际情况中我也用的是 do,而且我也是无条件 close 的
    EthanDon
        47
    EthanDon  
       Dec 24, 2019
    @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)
    }
    有什么区别吗。。。
    aliipay
        48
    aliipay  
       Dec 24, 2019
    @EthanDon
    看了下文档,get 方法也是判断是否 err 后再 close 的,和 do 一样。 之前说法是有问题。
    在 do/get 返回 error 时候,resp 是个 nil,不能调用 body.close 的。
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   5956 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 111ms · UTC 02:51 · PVG 10:51 · LAX 19:51 · JFK 22:51
    ♥ Do have faith in what you're doing.