需配置http.Client的Transport参数(如MaxIdleConns、Timeout等),用sync.WaitGroup+带缓冲channel控制并发,预分配数组存耗时再离线算P95/P99,并调高ulimit及避免localhost压测以确保结果可信。

怎么用 net/http 发起并发 HTTP 请求而不崩掉
Go 的 http.Client 本身是线程安全的,但默认配置在压测场景下会迅速吃光系统资源:连接复用没开、超时没设、最大空闲连接数太小。不调这些,跑个几百并发就卡住或报 too many open files。
- 必须显式设置
Transport:启用连接复用(MaxIdleConns和MaxIdleConnsPerHost至少设为 100+) - 一定要设
Timeout,否则慢请求堆积导致 goroutine 泄漏;推荐分层设:Timeout控总耗时,KeepAlive和TLSHandshakeTimeout防握手卡死 - 别用全局默认 client——每个压测任务应 new 独立
http.Client,避免不同任务间 timeout 或 transport 配置互相干扰
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 200,
MaxIdleConnsPerHost: 200,
IdleConnTimeout: 30 * time.Second,
},
}如何控制并发数且不漏计数(goroutine + channel 经典陷阱)
用 for i := 0; i < n; i++ { go req() } 看似简单,但容易漏等 goroutine 结束,或因 channel 缓冲不足导致发送阻塞——尤其当 QPS 高、响应时间波动大时,req() 函数里写 ch <- result 可能永远卡住。
- 用
sync.WaitGroup确保所有请求发完再关闭结果 channel - 结果 channel 必须带缓冲(容量 ≥ 并发数),否则第一个慢响应就会堵住后续 goroutine 的发送
- 别在 goroutine 里直接
close(ch)——只能由主 goroutine 在wg.Wait()后 close,否则可能 panic
怎么统计 P95/P99 延迟又不拖慢压测过程
压测时每条请求都做排序或插入堆,性能损耗极大;但用固定大小数组 + 计数桶(如按毫秒分桶)又怕精度不够。折中方案是采样 + 后处理,但得保证采样不偏斜。
- 不要在 hot path(即每个请求完成回调里)做浮点运算或切片扩容;用预分配的
[]int64存原始耗时,压测结束后再排序算分位数 - 如果请求数超百万,内存吃紧,改用
tdigest类库(如github.com/influxdata/tdigest),它用 O(log n) 空间近似分位数,误差可控在 1% 内 - 注意:
time.Since()返回的是time.Duration,存前先转成int64毫秒值,避免反复类型转换
为什么本地压测结果和线上差距大,连 localhost 都压不出高 QPS
不是代码问题,是环境限制。Mac/Linux 默认文件描述符限制(ulimit -n)通常只有 256 或 1024,而每个 HTTP 连接至少占 1 个 fd;加上 DNS 解析、TLS 握手等中间状态,实际能并发的连接远低于你设的数值。
立即学习“go语言免费学习笔记(深入)”;
- 压测前务必运行
ulimit -n 65536(Linux/macOS),Windows WSL 同理;Docker 容器需加--ulimit nofile=65536:65536 - 避免压测目标设为
localhost:loopback 虽快,但绕过真实网络栈,无法反映 TCP 拥塞控制、丢包重传等行为;改用本机 IP(如192.168.x.x)或另一台机器 - Go runtime 的
GOMAXPROCS默认等于 CPU 核数,但压测工具多数是 I/O 密集型,设太高反而增加调度开销;保持默认或设为runtime.NumCPU() * 2即可
真正难的不是写出来,是让数字可信——每个 http.Client 配置、每处 time.Now() 打点位置、甚至 ulimit 的生效范围,都会悄悄扭曲结果。











