必须手动构造禁用复用的http.client并用time.ticker控频:设maxidleconns=0、maxidleconnsperhost=0、idleconntimeout=0、forceattempthttp2=false,配合chan+worker实现稳定qps。

直接复用 http.Client 并禁用连接池复用,否则压测结果完全失真。真实接口压测必须模拟“新用户首次访问”行为,而默认的 http.Client 会复用 TCP 连接、DNS 缓存和 TLS 会话,掩盖服务端真实瓶颈。本地跑出 5000 QPS,上线后可能连 1000 都不到。
为什么不能直接用 http.Get 发起压测请求
每次调用 http.Get 都会新建一个未配置的 http.Client,其底层 http.Transport 默认关闭连接复用(MaxIdleConns = 0),但依然会触发 DNS 缓存、TLS 会话复用和 TIME_WAIT 积压。更严重的是:它无法控制并发节奏,裸写 go http.Get() N 次等于瞬间拉起 N 个 goroutine,测的不是服务吞吐,是你本机网络栈和调度器的崩溃点。
- 错误现象:
dial tcp: lookup xxx: no such host(DNS 解析失败)、too many open files(文件描述符耗尽)、延迟毛刺剧烈且不可复现 - 正确做法:手动构造带调优参数的
http.Client,并显式禁用复用关键项 - 关键配置项(必须设):
MaxIdleConns = 0、MaxIdleConnsPerHost = 0、IdleConnTimeout = 0、ForceAttemptHTTP2 = false - 额外建议:对 HTTPS 接口,设置
TLSClientConfig = &tls.Config{InsecureSkipVerify: true}避免证书校验开销(仅限测试环境)
如何用 sync.WaitGroup + time.Ticker 控制稳定并发节奏
单纯限制 goroutine 数量(如 semaphore)只能控“峰值”,无法控“速率”。真实用户是按固定频率发起请求的,比如 100 QPS,不是同时发 100 个再等 1 秒。
- 用
time.Ticker替代time.Sleep:精度更高,且能自动补偿请求耗时波动 - 生产者-消费者模型更可靠:一个 ticker goroutine 定期往
chan struct{}写信号,N 个 worker 从 channel 读取并执行请求 - 示例节选:
reqChan := make(chan struct{}, tps) // 缓冲区大小设为 TPS,防阻塞 go func() { ticker := time.NewTicker(time.Second / time.Duration(tps)) defer ticker.Stop() for i := 0; i - 注意:
concurrency应 ≥tps,否则 channel 会积压;若concurrency远大于tps,大量 goroutine 空转浪费调度资源
压测结果里真正该盯住的三个数字
终端输出的 Avg Latency 和 Requests/sec 是最误导人的两个指标。它们掩盖长尾、忽略错误分布、对瞬时抖动不敏感。
- 必须看
P95和P99延迟:如果 P95 是 200ms 而平均才 80ms,说明 5% 的请求已明显恶化 - 必须统计
Non-2xx响应数:不是所有错误都抛异常,429、503、超时返回的 0 状态码都要计入失败率 - 必须记录
total_requests与completed_requests差值:这个差值就是“被丢弃的请求”,反映服务端队列是否溢出或连接被主动拒绝 - 工具推荐:用
vegeta attack -rate=100 -duration=30s | vegeta report -type=json直接得结构化数据,避免人工 parse 终端
最容易被忽略的一点:压测机和被测服务必须分机器部署。localhost 压测绕过大部分 TCP 栈和网卡中断,结果比真实链路乐观 30% 以上;同一台机器还可能因 CPU/内存争抢导致服务端 GC 频繁或调度延迟,把问题归错到代码上。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











