多个http请求会出现“无法分配请求的地址”的错误,除非加快速度。

4

使用以下客户端代码(并在此主机上的8088端口上启动一个侦听web服务器),我很少能够获得超过23000个访问,然后client.Get()就会出现以下错误:

panic: Get http://localhost:8088/: dial tcp 127.0.0.1:8088: can't assign requested address

奇怪的是,如果我增加计时器延迟(即从毫秒级别增加到微秒级别),需要更多的请求才会出现错误,可能需要170,000或更多次请求。
通过查看网络流量,每个客户端连接只使用了几次就断开连接(即客户端发送FIN)。因此很明显它正在创建许多TCP连接并且溢出了套接字表。鉴于Golang HTTP文档中默认启用了keepalives,我不希望出现这种情况。内核跟踪显示,在关闭之前基础套接字没有发出任何错误(除了EAGAIN,这是预期的,并不总是在套接字关闭之前发生)。
这是在OSX(14.4.0)上使用Go 1.4.2进行的。为什么客户端连接不能一直重用呢?
package main

import (
    "io/ioutil"
    "net/http"
    "runtime"
    "sync"
    "time"
)

var reqnum = 0

func hit(client *http.Client) {
    resp, err := client.Get("http://localhost:8088/")
    if err != nil {
        println(reqnum)
        panic(err)
    }
    defer resp.Body.Close()
    _, err = ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }
    reqnum++ // not thread safe, but shouldn't cause errors.
}

func main() {
    var wg sync.WaitGroup
    runtime.GOMAXPROCS(runtime.NumCPU())
    client := &http.Client{}
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            ticker := time.NewTicker(time.Microsecond * 1000)
            for j := 0; j < 120000; j++ {
                <-ticker.C
                hit(client)
            }
            ticker.Stop()
        }()
    }
    wg.Wait()
}

哦,好问题。我无法解释那种确切的行为,但是由于TCP TIME_WAIT,类似的情况可能会发生。(端口在使用后会保留一段时间,如果端口用尽了,你就会遇到麻烦。)我会尝试找到相关资源来解释这个问题。 - undefined
理论上,我认为Go对于保持连接的支持应该可以保持连接数量较少。如果服务器只允许在一个连接上进行10个请求(或者Go端有这样的限制),那么你可能会在每个工作线程上建立12k个连接,并且由于处于TIME_WAIT状态的连接的“幽灵”存在,你不能立即重用它们的端口一段时间。但是,这与更高的速率有时运行成功不相符;那样的话,更长的延迟反而会对你有帮助,因为它能为旧连接的TIME_WAIT过期提供足够的时间窗口。 - undefined
最后,作为额外的数据点(来自上面链接的问题),你有16384个端口用于连接。如果它们全部被使用或处于TIME_WAIT状态,那么你就陷入了困境。你可以尝试降低MSL的值,看看是否能神奇地解决问题(至少给出一个诊断结果),或者深入研究服务器上keepalive的工作原理(或Go标准库中的工作原理),或者尝试观察连接实际发生的时间(在Linux上可以使用strace,不确定在OS X上该如何操作)。 - undefined
1
看了一下源文件,有一个可能性是MaxIdleConnsPerHost。这个限制是针对每个Client的,并且你在十个工作线程中共享一个。这也可以解释为什么有时候非常快的计时器会完成测试:如果你更快地让连接重新工作1000倍,那么就不太可能有3个以上的连接处于空闲状态。一个简单的测试是人为增加最大连接数或者每个工作线程的客户端是否会改变连接行为。 - undefined
目前,osx将释放的回环端口放入一个(据我所知)未标记的TIME_WAIT队列中,持续15秒。您在netstat中看不到它们,但是在15秒内无法建立超过16384个连接而不出现端口分配错误。 - undefined
显示剩余4条评论
1个回答

10
拨号过程中出现的错误can't assign requested address是由于本地短暂端口已用尽而导致的。您之所以会用尽端口,只是因为您建立连接太快了。当您加快连接速度时,就会开始捕获返回池中的空闲连接,然后才关闭它们。在拨号期间,有一个代码路径可以捕获这些新的空闲连接,以更快地返回连接,但无法确定每次都能捕获这些连接。

既然您只连接一个主机(如评论中所讨论的),那么您需要将Transport.MaxIdleConnsPerHost设置得更高。您需要找到一个平衡点,在打开太多连接和开始过快地回收它们之间。

甚至可以在客户端上设置信号量,以防止太多同时连接,这会导致连接再次过快地回收。


就是这样!在传输中将MaxIdleConnsPerHost设置为10,这样在程序的整个生命周期中只会建立10个连接。谢谢! - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接