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() }
额外优化建议
- 调整HTTP客户端连接池:默认的
http.Transport连接池大小有限,可以手动配置提升并发能力:
client := http.Client{ Timeout: 30 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 100, IdleConnTimeout: 90 * time.Second, DisableKeepAlives: false, // 保持长连接,减少握手开销 }, }
- 避免频繁创建Client:
http.Client是线程安全的,可以在所有goroutine之间复用,不用每个goroutine都创建。 - 优化错误处理:当前代码遇到错误直接
os.Exit(1),可以改成记录日志后继续运行,避免单个请求失败导致整个客户端崩溃。
内容的提问来源于stack exchange,提问作者Mohammad Hadi Setak
相关产品推荐
相关产品推荐

