Golang中HTTP客户端的并发性能影响

Golang中HTTP客户端的并发性能影响 你好,

我对HTTP客户端有一些疑问。

我有一个HTTP服务,其处理程序会同步调用另一个HTTP服务(服务A)。(我称之为案例1)。

func withoutGoroutine(w http.ResponseWriter, r *http.Request) {

  httpGetA()

  _, err := w.Write([]byte(``))
  if err != nil {
    log.Printf("%v", err)
  }
}

同一个服务有另一个处理程序,它执行与案例1相同的操作,但额外使用一个不同的HTTP客户端异步调用另一个不同的服务(服务B)。(我称之为案例2)。

func withGoroutine(w http.ResponseWriter, r *http.Request) {

  httpGetA()

  _, err := w.Write([]byte(``))
  if err != nil {
    log.Printf("%v", err)
  }

  go httpGetB()
}

我对这些处理程序进行了一些负载测试,结果如下:

Case 1

    ab -n 500000 -c 50 0.0.0.0:8080/withoutgoroutine
    ...
    Percentage of the requests served within a certain time (ms)
      50%      3
      66%      4
      75%      4
      80%      4
      90%      5
      95%      7
      98%     10
      99%     11
     100%     38 (longest request)

Case 2

    ab -n 500000 -c 50 0.0.0.0:8080/withgoroutine
    ...
    Percentage of the requests served within a certain time (ms)
      50%      5
      66%      5
      75%      6
      80%      6
      90%      8
      95%     10
      98%     14
      99%     16
     100%     42 (longest request)

为什么案例2的响应更慢? 我原本期望响应时间相同。

为了确保问题不在于goroutine的创建,我创建了第三个案例:案例1加上创建goroutine但不进行HTTP调用。

func withSleepyGoroutine(w http.ResponseWriter, r *http.Request) {

  httpGetA()

  _, err := w.Write([]byte(``))
  if err != nil {
    log.Printf("%v", err)
  }

  go func() {
    time.Sleep(1 * time.Millisecond)
  }()
}

在案例2和案例3中,goroutine是在HTTP响应发送之后创建的。

我在不同的机器上进行了负载测试,以排除网络问题。

完整的源代码可在此Github仓库中找到。

GitHub GitHub

非常感谢你的帮助。

此致


更多关于Golang中HTTP客户端的并发性能影响的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

尝试对这两种情况进行性能分析。

如果你正在运行一个HTTP服务器,最简单的方法是添加 net/pprof 钩子

然后你可以使用 go tool pprof 来发现关于你服务器的各种有趣信息。

更多关于Golang中HTTP客户端的并发性能影响的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


感谢您的回答。

我添加了gops代理来获取性能剖析和跟踪数据,例如下面这些同步阻塞剖析的图片:

Imgur

Imgur

但是,我不太确定能从这些数据中得出什么结论。

或者说,这反而引出了更多问题。例如,在相同的负载测试时间内,为什么情况2中运行时使用的时间是情况1的两倍?

再次感谢您的帮助。

在Golang中,HTTP客户端的并发性能主要受net/http包中默认的DefaultTransport配置影响。案例2性能下降的核心原因是连接复用限制

问题分析

http.DefaultClient使用http.DefaultTransport,其关键配置如下:

MaxIdleConns:          100,
MaxIdleConnsPerHost:   2,  // 关键限制
MaxConnsPerHost:       0,
IdleConnTimeout:       90 * time.Second,

根本原因

  1. 连接池竞争:案例2中每个请求都创建goroutine调用服务B,导致大量并发HTTP请求
  2. MaxIdleConnsPerHost=2:每个主机最多只能保持2个空闲连接
  3. 连接建立开销:超过限制后需要频繁创建新TCP连接

解决方案

方案1:优化Transport配置

var customTransport = &http.Transport{
    MaxIdleConns:        100,
    MaxIdleConnsPerHost: 50,  // 提高每个主机的空闲连接数
    MaxConnsPerHost:     100, // 限制每个主机的总连接数
    IdleConnTimeout:     90 * time.Second,
    TLSHandshakeTimeout: 10 * time.Second,
}

var customClient = &http.Client{
    Transport: customTransport,
    Timeout:   30 * time.Second,
}

func httpGetB() {
    resp, err := customClient.Get("http://service-b:8080/")
    if err != nil {
        log.Printf("Error: %v", err)
        return
    }
    defer resp.Body.Close()
}

方案2:使用连接池和工作池

type WorkerPool struct {
    workers int
    tasks   chan func()
}

func NewWorkerPool(workers int) *WorkerPool {
    wp := &WorkerPool{
        workers: workers,
        tasks:   make(chan func(), 1000),
    }
    for i := 0; i < workers; i++ {
        go wp.worker()
    }
    return wp
}

func (wp *WorkerPool) worker() {
    for task := range wp.tasks {
        task()
    }
}

func (wp *WorkerPool) Submit(task func()) {
    select {
    case wp.tasks <- task:
    default:
        log.Println("Worker pool full, dropping task")
    }
}

// 全局工作池
var pool = NewWorkerPool(100)

func withGoroutineOptimized(w http.ResponseWriter, r *http.Request) {
    httpGetA()
    
    w.Write([]byte(""))
    
    pool.Submit(func() {
        httpGetB()
    })
}

方案3:批量处理异步请求

type BatchProcessor struct {
    batchSize int
    buffer    chan *http.Request
    client    *http.Client
}

func NewBatchProcessor(batchSize int) *BatchProcessor {
    bp := &BatchProcessor{
        batchSize: batchSize,
        buffer:    make(chan *http.Request, 10000),
        client:    customClient,
    }
    go bp.process()
    return bp
}

func (bp *BatchProcessor) process() {
    var batch []*http.Request
    ticker := time.NewTicker(100 * time.Millisecond)
    
    for {
        select {
        case req := <-bp.buffer:
            batch = append(batch, req)
            if len(batch) >= bp.batchSize {
                bp.flush(batch)
                batch = nil
            }
        case <-ticker.C:
            if len(batch) > 0 {
                bp.flush(batch)
                batch = nil
            }
        }
    }
}

func (bp *BatchProcessor) flush(batch []*http.Request) {
    var wg sync.WaitGroup
    for _, req := range batch {
        wg.Add(1)
        go func(r *http.Request) {
            defer wg.Done()
            bp.client.Do(r)
        }(req)
    }
    wg.Wait()
}

性能对比测试

func benchmarkTest() {
    // 测试不同配置的性能
    tests := []struct {
        name      string
        transport *http.Transport
    }{
        {
            name: "Default",
            transport: &http.Transport{
                MaxIdleConnsPerHost: 2,
            },
        },
        {
            name: "Optimized",
            transport: &http.Transport{
                MaxIdleConns:        100,
                MaxIdleConnsPerHost: 50,
                MaxConnsPerHost:     100,
            },
        },
    }
    
    for _, test := range tests {
        client := &http.Client{
            Transport: test.transport,
            Timeout:   30 * time.Second,
        }
        
        start := time.Now()
        var wg sync.WaitGroup
        for i := 0; i < 1000; i++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                resp, _ := client.Get("http://service-b:8080/")
                if resp != nil {
                    resp.Body.Close()
                }
            }()
        }
        wg.Wait()
        fmt.Printf("%s: %v\n", test.name, time.Since(start))
    }
}

监控指标

func monitorConnections() {
    go func() {
        for {
            time.Sleep(5 * time.Second)
            stats := customTransport.(*http.Transport)
            fmt.Printf("Idle connections: %d\n", len(stats.IdleConn))
        }
    }()
}

关键点:

  1. 默认的MaxIdleConnsPerHost=2是主要瓶颈
  2. 大量goroutine创建HTTP请求会导致连接竞争
  3. 优化Transport配置可以显著提升并发性能
  4. 使用工作池可以控制并发度,避免资源耗尽

建议根据实际负载调整MaxIdleConnsPerHostMaxConnsPerHost的值,并通过监控确认最佳配置。

回到顶部