You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go多协程客户端测服务器:RPS随协程数增加异常下降

问题根源

你遇到的核心问题是所有goroutine共享同一个http.Request对象。http.Client.Do方法在执行过程中会修改传入的Request实例(比如更新请求头、内部状态等),多个goroutine并发操作这个共享对象会触发严重的竞态条件——大量CPU被消耗在内部锁竞争和数据不一致的修复上,直接导致RPS暴跌、负载飙升。

当你用2个goroutine时,竞争还不算激烈,所以性能正常;但goroutine数量增加后,竞争指数级上升,性能直接崩了。而多开独立客户端进程时,每个进程的goroutine各自持有自己的Request,没有共享竞争,所以性能能叠加。

你后来加的全局锁虽然缓解了竞争,但相当于把所有请求串行化了,自然没法达到预期的并发性能。

修复方案

给每个goroutine创建独立的http.Request实例,或者在每次调用Do前克隆请求。以下是修复后的客户端代码:

package main

import (
    "fmt"
    "net/http"
    "os"
    "sync"
    "time"
)

func main() {
    requestURL := fmt.Sprintf("http://localhost:%d/home", 3333)
    var wg sync.WaitGroup
    goroutineCount := 4
    wg.Add(goroutineCount)

    for i := 0; i < goroutineCount; i++ {
        go func() {
            defer wg.Done()
            // 每个goroutine复用同一个Client(Client是线程安全的)
            client := http.Client{
                Timeout: 30 * time.Second,
            }
            for {
                // 每次请求创建独立的Request实例
                req, err := http.NewRequest(http.MethodGet, requestURL, nil)
                if err != nil {
                    fmt.Printf("client: could not create request: %s\n", err)
                    os.Exit(1)
                }
                _, err = client.Do(req)
                if err != nil {
                    fmt.Printf("client: error making request: %s\n", err)
                }
            }
        }()
    }
    wg.Wait()
}

如果需要复用基础请求的配置(比如自定义Header),可以用req.Clone来克隆请求:

package main

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "sync"
    "time"
)

func main() {
    requestURL := fmt.Sprintf("http://localhost:%d/home", 3333)
    var wg sync.WaitGroup
    goroutineCount := 4
    wg.Add(goroutineCount)

    // 创建基础请求模板
    baseReq, err := http.NewRequest(http.MethodGet, requestURL, nil)
    if err != nil {
        fmt.Printf("client: could not create request: %s\n", err)
        os.Exit(1)
    }
    // 可以在这里统一设置Header等配置
    baseReq.Header.Set("User-Agent", "load-test-client")

    for i := 0; i < goroutineCount; i++ {
        go func() {
            defer wg.Done()
            client := http.Client{
                Timeout: 30 * time.Second,
            }
            for {
                // 每次请求克隆独立的Request实例
                req := baseReq.Clone(context.Background())
                _, err = client.Do(req)
                if err != nil {
                    fmt.Printf("client: error making request: %s\n", err)
                }
            }
        }()
    }
    wg.Wait()
}

额外优化建议

  1. 调整HTTP客户端连接池:默认的http.Transport连接池大小有限,可以手动配置提升并发能力:
client := http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 100,
        IdleConnTimeout:     90 * time.Second,
        DisableKeepAlives:   false, // 保持长连接,减少握手开销
    },
}
  1. 避免频繁创建Client:http.Client是线程安全的,可以在所有goroutine之间复用,不用每个goroutine都创建。
  2. 优化错误处理:当前代码遇到错误直接os.Exit(1),可以改成记录日志后继续运行,避免单个请求失败导致整个客户端崩溃。

内容的提问来源于stack exchange,提问作者Mohammad Hadi Setak

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 19:35:00