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仓库中找到。
非常感谢你的帮助。
此致
更多关于Golang中HTTP客户端的并发性能影响的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于Golang中HTTP客户端的并发性能影响的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
感谢您的回答。
我添加了gops代理来获取性能剖析和跟踪数据,例如下面这些同步阻塞剖析的图片:


但是,我不太确定能从这些数据中得出什么结论。
或者说,这反而引出了更多问题。例如,在相同的负载测试时间内,为什么情况2中运行时使用的时间是情况1的两倍?
再次感谢您的帮助。
在Golang中,HTTP客户端的并发性能主要受net/http包中默认的DefaultTransport配置影响。案例2性能下降的核心原因是连接复用限制。
问题分析
http.DefaultClient使用http.DefaultTransport,其关键配置如下:
MaxIdleConns: 100,
MaxIdleConnsPerHost: 2, // 关键限制
MaxConnsPerHost: 0,
IdleConnTimeout: 90 * time.Second,
根本原因
- 连接池竞争:案例2中每个请求都创建goroutine调用服务B,导致大量并发HTTP请求
- MaxIdleConnsPerHost=2:每个主机最多只能保持2个空闲连接
- 连接建立开销:超过限制后需要频繁创建新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))
}
}()
}
关键点:
- 默认的
MaxIdleConnsPerHost=2是主要瓶颈 - 大量goroutine创建HTTP请求会导致连接竞争
- 优化Transport配置可以显著提升并发性能
- 使用工作池可以控制并发度,避免资源耗尽
建议根据实际负载调整MaxIdleConnsPerHost和MaxConnsPerHost的值,并通过监控确认最佳配置。

