http - 在 Go 中使用 DialContext 的性能问题

标签 http go

我使用 Go 的内置 http.Clientnet 进行了快速基准测试。使用 DialContext 时与不使用它时相比,会导致一些明显的性能问题。

我基本上是在尝试模仿我们公司的一个用例,其中当用于相同的事情时,http.Client 设置的性能远低于默认配置。我注意到注释 DialContext 部分可以让它运行得更快。

基准测试只是打开一个线程池(示例中为 8 个)来创建到简单 URL 的连接,使用大小与线程数 (8) 相同的缓冲 channel 。

这是带有 DialContext 的代码(2.266333805s):

func main() {
    var httpClient *http.Client

    httpClient = &http.Client{
        Transport: &http.Transport{
            DialContext: (&net.Dialer{
                Timeout:   3 * time.Second,
                KeepAlive: 30 * time.Second,
                DualStack: true,
            }).DialContext,
        },
    }

    url := "https://stackoverflow.com/"
    wg := sync.WaitGroup{}
    threads := 8
    wg.Add(threads)
    c := make(chan struct{}, threads)

    start := time.Now()
    for i := 0; i < threads; i++ {
        go func() {
            for range c {
                req, _ := http.NewRequest(http.MethodGet, url, nil)
                resp, err := httpClient.Do(req)
                if err == nil {
                    resp.Body.Close()
                }
            }
            wg.Done()
        }()
    }

    for i := 0; i < 200; i++ {
        c <- struct{}{}
    }
    close(c)
    wg.Wait()

    fmt.Println(time.Since(start))
}

输出时间为2.266333805s

这是没有 DialContext 的代码(731.154103ms):

func main() {
    var httpClient *http.Client

    httpClient = &http.Client{
        Transport: &http.Transport{
        },
    }

    url := "https://stackoverflow.com/"
    wg := sync.WaitGroup{}
    threads := 8
    wg.Add(threads)
    c := make(chan struct{}, threads)

    start := time.Now()
    for i := 0; i < threads; i++ {
        go func() {
            for range c {
                req, _ := http.NewRequest(http.MethodGet, url, nil)
                resp, err := httpClient.Do(req)
                if err == nil {
                    resp.Body.Close()
                }
            }
            wg.Done()
        }()
    }

    for i := 0; i < 200; i++ {
        c <- struct{}{}
    }
    close(c)
    wg.Wait()

    fmt.Println(time.Since(start))
}

输出时间为731.154103ms

多次运行程序后结果之间的差异是一致的。

有人知道为什么会发生这种情况吗?

谢谢!

编辑:所以我尝试了net/http/httptrace,并确保响应的正文已完全读取并关闭:

go func() {
    for range c {
        req, _ := http.NewRequest(http.MethodGet, url, nil)
        req = req.WithContext(httptrace.WithClientTrace(req.Context(), &httptrace.ClientTrace{
            GotConn: t.gotConn,
        }))
        resp, _ := httpClient.Do(req)
        ioutil.ReadAll(resp.Body)
        resp.Body.Close()
    }
    wg.Done()
}()

使用 DialContext 与不使用它时的发现很有趣。

不使用 DialContext:

time taken to run 200 requests: 5.639808793s
new connections: 1
reused connections: 199

使用 DialContext:

time taken to run 200 requests: 5.682882723s
new connections: 8
reused connections: 192

速度更快...但为什么一个打开 8 个新连接,而另一个只打开 1 个?

最佳答案

获得如此大差异的唯一方法是一种传输正在重用连接,而另一种则没有。为了确保您可以重用连接,您必须始终读取响应正文。在某些情况下,连接可能会在不显式读取正文的情况下被重用,但这永远无法保证,并且取决于许多因素,例如远程服务器关闭连接、响应是否由 Transport 完全缓冲>,以及请求是否有上下文。

net/http/httptrace包可以让您深入了解许多请求的内部结构,包括连接是否被重用。请参阅https://blog.golang.org/http-tracing例如。

设置DisableKeepAlive将始终阻止连接被重用,从而使两者同样变慢。始终像这样阅读响应将使两者同样快速:

req, _ := http.NewRequest(http.MethodGet, url, nil)
resp, err := httpClient.Do(req)
if err != nil {
    // handle error
    continue
}
io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close()

如果您想在断开连接之前限制可以读取的数据量,您可以简单地将正文包裹在 io.LimitedReader 中。

关于http - 在 Go 中使用 DialContext 的性能问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62071857/

相关文章:

go - 为什么即使达到 EOF io.Pipe() 也会继续阻塞?

regex - 重写正则表达式而不使用否定

go - 文件不在使用 proto_path 指定的任何路径中

http - PHP_SELF vs PATH_INFO vs SCRIPT_NAME vs REQUEST_URI

file - 下载文件的最简单方法?

Java http 下载损坏文件

api - 404 与 200 相关实体,RESTful

swift - 如何将引号(“)包含到 Swift 中 HTTP Head 字段使用的字符串中

戈兰 : Custom template "block" functions?

GORM pq 连接太多