go test -bench 不适合测 http 服务吞吐量,它绕过 tcp 栈、连接池、tls 和 accept 队列,仅反映 handler 内存执行速度;真实压测必须用 vegeta/wrk/ghz 并禁用 keep-alive,配合 pprof 分析与 x-test-flag 全链路透传。

别用 go test -bench 测 HTTP 服务吞吐量——它绕过 TCP 栈、连接池、TLS 和 accept 队列,测的不是你的服务,是 handler 在内存里跑多快。
go test -bench 只适合验证 handler 纯逻辑
它能快速确认 JSON 解析、路由匹配、中间件链执行是否高效,但前提是:禁用日志、mock 所有外部依赖、关闭真实中间件。否则你压的是 log.Printf 速度或 Redis 连接建立耗时。
-
b.ResetTimer()必须加在初始化之后、循环之前,否则httptest.NewRequest和httptest.NewRecorder的开销会污染结果 - 日志必须关掉:
log.SetOutput(io.Discard),否则延迟可能被拖慢几十倍 - 数据库、Redis、gRPC 调用一律 mock,用 interface + 依赖注入,避免结果不可复现
- 请求对象和响应记录器要在循环外创建,别在每次迭代里重复分配
真实链路压测必须用 vegeta/wrk/ghz 并禁用 Keep-Alive
HTTP 压测不是比谁发得快,而是看服务端在真实网络压力下会不会卡死、堆积、超时、OOM。连接复用会掩盖 TIME_WAIT 泛滥、accept 队列溢出、连接池打满等关键瓶颈。
-
wrk示例(强制每请求新建连接):wrk -c 200 -d 30s -t 4 --timeout 5s -H "Connection: close" http://localhost:8080/api/users -
vegeta更适合带 body 和 header 的场景:echo "POST http://localhost:8080/api/login" | vegeta attack -body login.json -header "Content-Type: application/json" -rate 100 -duration 30s -timeout 5s -insecure | vegeta report -
ghz是 gRPC 唯一推荐方案:ghz --insecure --proto ./api.proto --call pb.UserService.GetUser -d '{"id":"123"}' -c 50 -z 30s localhost:50051,它自动管理连接池、支持 metadata 注入、输出 P90/P99 和错误码分布 - 所有客户端必须设
DisableKeepAlives: true(vegeta默认关,wrk默认开,ghz用--keepalive=false)
压测时必须边跑边采 pprof,否则等于闭眼开车
没采集 profile 的压测数据不可信。CPU 高不等于 handler 慢,可能是 GC 频繁、锁竞争或系统调用阻塞;heap 暴涨不一定是泄漏,也可能是未关闭的 http.Response.Body 或日志缓冲区。
- 启动服务时加
import _ "net/http/pprof",并起独立监听端口:http.ListenAndServe(":6060", nil) - 压测中同时抓三类 profile:
wget "http://localhost:6060/debug/pprof/profile?seconds=30" -O cpu.pprof、/heap、/goroutine?debug=2 - 每次压测前清空 page cache、重启服务、手动触发两次
runtime.GC(),避免缓存干扰和 GC 周期污染数据 - 至少跑三次,取中位数;单次结果波动大时,说明服务本身不稳定,不是压测工具问题
全链路压测的关键是 X-Test-Flag 必须手动透传到底层 DAO
压测流量一旦写进主库或覆盖 Redis 缓存,就不是性能测试,是事故预演。Golang 没有运行时 AOP,所有透传都得靠显式编码,漏一层就污染生产。
- HTTP 入口第一行就要检查:
if flag := r.Header.Get("X-Test-Flag"); flag == "1" { ctx = context.WithValue(r.Context(), testKey, true) },testKey必须是私有 struct 类型,不能是字符串 - gRPC 场景必须在 unary interceptor 中用
metadata.FromIncomingContext提取,并用metadata.AppendToOutgoingContext回写,RoundTrip 不生效 - DAO 层不能硬编码表名前缀,而要根据
ctx.Value(testKey)动态选*sql.DB实例;Redis 封装Get方法,在内部自动拼接stress_user:1001键 - Nginx 默认 header 大小限制 4KB,若同时传 trace ID、scene ID 和压测标识,需调大
large_client_header_buffers,否则第一跳就丢标
最容易被忽略的点是:pprof 数据必须和请求流量对齐。你抓了 30 秒 CPU profile,但压测只跑了 25 秒,或者 profile 期间有后台定时任务触发,那分析结果就完全失真。时间戳、QPS 曲线、profile 抓取窗口,三者必须严格同步。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











