应复用调优后的http.client而非http.get:需配置transport的maxidleconns、maxidleconnsperhost(设相同值)、idleconntimeout和tlshandshaketimeout,并配合适当并发控制与系统ulimit调优,避免time_wait堆积、dns重复解析及tls握手开销。

用 net/http 发起并发请求时,别直接用 http.Get
直接调用 http.Get 会为每次请求新建一个 http.Client,默认复用连接池(Transport)被禁用,导致大量 TIME_WAIT 连接、DNS 重复解析、TLS 握手开销激增。压测结果完全失真,QPS 虚低,还容易触发 dial tcp: lookup xxx: no such host 或 too many open files。
正确做法是复用一个带调优的 http.Client:
client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 200,
MaxIdleConnsPerHost: 200,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
},
}
-
MaxIdleConns和MaxIdleConnsPerHost必须设为相同值,否则后者会被前者截断 - Linux 下记得同步调大系统限制:
ulimit -n 65536,否则压测中途会卡在socket: too many open files - 如果目标接口走 HTTPS,
TLSHandshakeTimeout建议设为 5–10 秒,避免个别慢握手拖垮整体并发
用 sync.WaitGroup + chan 控制并发量,别用 for i := 0; i 直接启 goroutine
裸写 go req() N 次,等于一次性拉起 N 个 goroutine,不控制节奏。真实压测需要稳定并发数(如 100 QPS),否则测的不是服务吞吐,而是你本机调度器和网络栈的极限。
推荐用“生产者-消费者”模型:一个 goroutine 按固定频率发任务,多个 worker goroutine 消费执行:
reqChan := make(chan struct{}, concurrency)
for i := 0; i
- 用
chan struct{}仅作信号传递,零内存开销 -
totalRequests和tps分开配置,便于做阶梯压测(比如每 30 秒加 50 并发) - 别用
time.Sleep替代ticker,sleep 精度差,且无法应对请求耗时波动
记录响应时间必须用 time.Now().UnixNano(),别用 time.Since() 做聚合统计
压测工具的核心输出是 P90/P95/P99 延迟、错误率、QPS。如果在每个请求里只存一个 time.Since(start) 的 time.Duration,后续做分位数计算时还得转成纳秒整数——看似省事,实则埋坑:Go 的 Duration 是 int64(纳秒级),但部分统计库(如 golang.org/x/exp/rand 分位数实现)要求输入是原始纳秒值,类型不匹配易出错;更关键的是,time.Since 在高并发下有微小性能损耗(内部调用 now() 两次)。
建议统一用纳秒整型记录:
start := time.Now().UnixNano() _, err := client.Do(req) costNs := time.Now().UnixNano() - start
- 所有耗时统一单位为纳秒,后续排序、分位计算、导出 CSV 都无类型转换成本
- 错误统计别只记
err != nil,要区分网络层错误(如net/http: request canceled)、服务端 HTTP 错误码(如502、429)、业务自定义错误字段(需解析 body) - 避免在压测循环里打印日志——
fmt.Println是锁操作,1000 QPS 下日志 I/O 成瓶颈
生成报告时慎用 sort.Int64Slice 做分位数,大数据量下内存和 CPU 吃紧
万级请求延迟数据(每个 8 字节 int64)才 80KB,但如果你压到百万请求,原始耗时切片就占 8MB,sort.Sort 还要额外分配临时空间,GC 压力明显。更糟的是,很多开发者直接把全部 costNs 存进 slice 再排序求 P99——这在长时间压测或高频采集中极易 OOM。
更实用的做法是流式分位数估算:
- 用
github.com/axiomhq/hyperloglog或github.com/leoluk/perks的quantile包,支持动态插入 + O(1) 内存占用 - 或者简单粗暴:按 10ms 精度做直方图桶(
map[int]int),key 是costNs / 1e7(即 10ms 桶宽),再遍历桶算累计百分比——P99 就是累计达到 99% 的桶右边界 - 最终报告至少包含:总请求数、成功数、错误率、最小/最大/平均耗时(ms)、P50/P90/P95/P99、各 HTTP 状态码分布
真正难的不是发请求,是让每个耗时数字可信、可复现、能对齐线上监控指标。比如你压出来 P99=200ms,但 APM 里显示是 230ms,大概率是没关掉客户端的 DNS 缓存或用了不同 Hosts 解析路径——这种细节不抠,压测就只是跑个数字游戏。











