Golang中net/http的keep alive功能失效问题解决方案

Golang中net/http的keep alive功能失效问题解决方案 我编写了两个测试单元。在下面的测试单元中,keep alive 确实生效了。

func BenchmarkNetHTTPClientBase2(b *testing.B) {
    client := http.Client{Transport: http.DefaultTransport}

    for i := 0; i < b.N; i++ {
        pingReq, _ := http.NewRequestWithContext(context.Background(), "GET", "http://127.0.0.1:7777/ping", nil)
        pingRes, err := client.Do(pingReq)
        if err != nil {
            b.Error(err)
        } else {
            _, err = ioutil.ReadAll(pingRes.Body)
            if err != nil {
                b.Error(err)
            }
            pingRes.Body.Close()
        }
    }
}

但是当我做了一些修改,添加了 RunParallel 后,keep alive 就不起作用了。

func BenchmarkNetHTTPClientBase1(b *testing.B) {
    client := http.Client{Transport: http.DefaultTransport}

    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            pingReq, _ := http.NewRequestWithContext(context.Background(), "GET", "http://127.0.0.1:7777/ping", nil)
            pingRes, err := client.Do(pingReq)
            if err != nil {
                b.Error(err)
            } else {
                _, err = ioutil.ReadAll(pingRes.Body)
                if err != nil {
                    b.Error(err)
                }
                pingRes.Body.Close()
            }
        }
    })
}

有什么想法吗?谢谢。


更多关于Golang中net/http的keep alive功能失效问题解决方案的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中net/http的keep alive功能失效问题解决方案的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


问题在于 RunParallel 会并发执行多个 goroutine,而 http.DefaultTransport 是全局共享的。当多个 goroutine 并发使用同一个 http.Client 时,连接池的竞争可能导致 keep-alive 行为异常。

以下是解决方案:

  1. 为每个 goroutine 创建独立的 Transport(推荐):
func BenchmarkNetHTTPClientBase1(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        // 每个 goroutine 使用独立的 Transport
        tr := &http.Transport{
            MaxIdleConns:        100,
            MaxIdleConnsPerHost: 100,
            IdleConnTimeout:     90 * time.Second,
        }
        client := &http.Client{Transport: tr}
        
        for pb.Next() {
            pingReq, _ := http.NewRequestWithContext(context.Background(), 
                "GET", "http://127.0.0.1:7777/ping", nil)
            pingRes, err := client.Do(pingReq)
            if err != nil {
                b.Error(err)
            } else {
                _, err = io.ReadAll(pingRes.Body)
                if err != nil {
                    b.Error(err)
                }
                pingRes.Body.Close()
            }
        }
    })
}
  1. 使用 sync.Pool 重用 Transport(性能优化):
func BenchmarkNetHTTPClientBase1(b *testing.B) {
    transportPool := &sync.Pool{
        New: func() interface{} {
            return &http.Transport{
                MaxIdleConns:        100,
                MaxIdleConnsPerHost: 100,
                IdleConnTimeout:     90 * time.Second,
            }
        },
    }
    
    b.RunParallel(func(pb *testing.PB) {
        tr := transportPool.Get().(*http.Transport)
        defer transportPool.Put(tr)
        
        client := &http.Client{Transport: tr}
        
        for pb.Next() {
            pingReq, _ := http.NewRequestWithContext(context.Background(),
                "GET", "http://127.0.0.1:7777/ping", nil)
            pingRes, err := client.Do(pingReq)
            if err != nil {
                b.Error(err)
            } else {
                _, err = io.ReadAll(pingRes.Body)
                if err != nil {
                    b.Error(err)
                }
                pingRes.Body.Close()
            }
        }
    })
}
  1. 调整全局 Transport 参数
func init() {
    http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = 100
}

func BenchmarkNetHTTPClientBase1(b *testing.B) {
    client := &http.Client{Transport: http.DefaultTransport}
    
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            pingReq, _ := http.NewRequestWithContext(context.Background(),
                "GET", "http://127.0.0.1:7777/ping", nil)
            pingRes, err := client.Do(pingReq)
            if err != nil {
                b.Error(err)
            } else {
                _, err = io.ReadAll(pingRes.Body)
                if err != nil {
                    b.Error(err)
                }
                pingRes.Body.Close()
            }
        }
    })
}

关键点:

  • RunParallel 默认使用 GOMAXPROCS 个 goroutine 并发执行
  • 默认的 MaxIdleConnsPerHost 是 2,并发 goroutine 超过这个数时,多余的连接会被关闭
  • 使用 io.ReadAll 替代已废弃的 ioutil.ReadAll
  • 确保正确关闭响应体以释放连接回连接池

第一种方案是最直接有效的解决方案,它为每个并发 goroutine 提供了独立的连接池,避免了竞争条件。

回到顶部