V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
tim0991
V2EX  ›  Go 编程语言

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

  •  
  •   tim0991 · 2019-12-23 15:02:03 +08:00 · 7540 次点击
    这是一个创建于 1779 天前的主题,其中的信息可能已经有所发展或是发生改变。

    代码背景

    使用 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 条回复    2019-12-24 13:01:43 +08:00
    guonaihong
        1
    guonaihong  
       2019-12-23 15:08:40 +08:00
    用一个全局的 http.Client 就行。不需要每次 new 个新的。
    opengps
        2
    opengps  
       2019-12-23 15:09:12 +08:00
    文件跟数据库是两回事,你这并行验证的需求要用的不是文件数据,而是数据库
    tim0991
        3
    tim0991  
    OP
       2019-12-23 15:11:51 +08:00
    @guonaihong 难道不是代理 ip 不同 transport 不同吗? transport 不同还能用同一个 client 吗?
    tim0991
        4
    tim0991  
    OP
       2019-12-23 15:13:02 +08:00
    @opengps 谢谢指点 但就事论事 想了解一下 为什么以及如何解决文中的 goroutine 泄露问题
    EthanDon
        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/
    tim0991
        6
    tim0991  
    OP
       2019-12-23 15:17:10 +08:00
    @EthanDon 你好 谢谢你的回复 首先 body 我关了,其次我给 http.client 设置了超时, 最后 我在问题的结尾留了代码地址 如果你有空可以看看 帮忙指点一下的话感激不尽
    lishunan246
        7
    lishunan246  
       2019-12-23 15:25:34 +08:00
    这跟 goroutine 和 http 应该没有关系。
    单纯是 TIME_WAIT 的连接太多了。
    guonaihong
        8
    guonaihong  
       2019-12-23 15:26:42 +08:00
    虽然没有调试代码,但是,起的 go 程数是 ulimit -n 的,会不会太多?可以把控制 go 程的代码 queueCh <- true 放到 go 程外面。
    tim0991
        9
    tim0991  
    OP
       2019-12-23 15:27:57 +08:00
    @lishunan246 那请问 1. 如何控制 time wait 数量? 2 如何主动关闭 time wait?
    guonaihong
        10
    guonaihong  
       2019-12-23 15:28:52 +08:00
    7 楼说的也是一种可能,可以打开快速回收优化下。
    tim0991
        11
    tim0991  
    OP
       2019-12-23 15:30:04 +08:00
    @guonaihong 谢谢 我生成环境 channel 是在 goroutine 外的,这个是临时准备用来测试的,然后 ulimit -n 我设置的是 10000W 然后 channel 长度就是 9000 这应该不算长吧,现在只有单个进程
    index90
        12
    index90  
       2019-12-23 15:31:53 +08:00
    试一下在关闭之前:
    io.Copy(ioutil.Discard, resp.Body)
    monsterxx03
        13
    monsterxx03  
       2019-12-23 15:34:11 +08:00
    queueCh <- true 这行要放在 go func() 之前, 不然你希望的阻塞不会生效的.

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

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

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

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

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

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

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

    话说我在 A 城市网络,即使 size 减半也遇到 too many open file,换到 B 城市网络,即使不减半也不会有问题。
    tim0991
        43
    tim0991  
    OP
       2019-12-24 09:54:29 +08:00
    @SunRunAway 同问
    tim0991
        44
    tim0991  
    OP
       2019-12-24 10:07:54 +08:00
    @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  
       2019-12-24 10:35:01 +08:00
    @EthanDon 所以,仔细想想, 为什么 get/post 需要无条件 close,do 却不用。
    EthanDon
        46
    EthanDon  
       2019-12-24 11:57:09 +08:00
    @aliipay 实际情况中我也用的是 do,而且我也是无条件 close 的
    EthanDon
        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)
    }
    有什么区别吗。。。
    aliipay
        48
    aliipay  
       2019-12-24 13:01:43 +08:00
    @EthanDon
    看了下文档,get 方法也是判断是否 err 后再 close 的,和 do 一样。 之前说法是有问题。
    在 do/get 返回 error 时候,resp 是个 nil,不能调用 body.close 的。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1238 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 23:17 · PVG 07:17 · LAX 15:17 · JFK 18:17
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.