b.runparallel测不出真实并发性能,因其默认仅用gomaxprocs个goroutine且共享b.n计数器引发锁竞争;适用场景限于无状态纯函数压测;手写并行应优先用sync.waitgroup。

为什么 b.RunParallel 测不出真实并发性能?
它默认只用 GOMAXPROCS 个 goroutine,且不控制协程生命周期——你跑 1000 并发,实际可能只有 8 个在跑,其余排队等调度。更关键的是,b.RunParallel 内部用的是共享的 b.N 计数器,所有 goroutine 竞争一个整数,测的其实是原子操作+锁竞争开销,不是你的业务逻辑。
- 典型现象:
BenchmarkFoo-16 1000000 1200 ns/op,但换成手写sync.WaitGroup+go后反而快 3 倍 - 适用场景:仅适合极轻量、无状态、无共享资源的纯函数压测(比如
json.Marshal) - 参数陷阱:
b.RunParallel不接受并发数参数,只接受一个func(*testing.B),内部并发度由testing包硬编码决定(当前版本是runtime.GOMAXPROCS(0))
手写并行基准测试时,sync.WaitGroup 和 chan 怎么选?
优先用 sync.WaitGroup:启动快、无缓冲区管理负担、语义清晰。除非你要做请求限流或背压控制,否则别碰 chan——它会引入额外调度延迟和 GC 压力。
-
WaitGroup必须在 goroutine 启动前Add,不能放 inside goroutine,否则可能Wait永远不返回 - 别在每个 goroutine 里调
b.ResetTimer(),只在所有 goroutine 启动完毕后调一次 - 示例结构:
func BenchmarkMyHandler_Parallel(b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { var wg sync.WaitGroup for j := 0; j < 100; j++ { // 并发数 wg.Add(1) go func() { defer wg.Done() myHandler() }() } wg.Wait() } }
b.N 在并行测试里到底代表什么?
它代表外层循环次数,不是总请求数,也不是并发数。如果你写 for i := 0; i 然后每次启 100 goroutine,那总执行次数是 <code>b.N × 100,但 testing 包只按 b.N 折算 ns/op——结果会虚高 100 倍。
- 正确做法:把
b.N当作「总请求数」来分摊,例如total := b.N; perGoroutine := total / concurrency - 必须校验
concurrency ,否则 <code>perGoroutine为 0,goroutine 空转 - 如果业务有初始化开销(如建连接),把它提到
b.ResetTimer()之前,否则会被计入耗时
压测时 CPU 利用率上不去,八成是卡在 I/O 或锁上
Go 基准测试默认不显示 CPU 使用率,但 go test -cpuprofile=cpu.pprof 能快速定位瓶颈。常见卡点:HTTP client 复用不当、数据库连接池过小、log.Printf 直接打屏、甚至 time.Now() 频繁调用(在某些内核版本下有锁)。
- 检查
net/http.DefaultClient是否被复用——没复用的话,每次新建 Transport,DNS 解析+TLS 握手全重来 - 用
pprof.Lookup("mutex").WriteTo看锁竞争热点,尤其注意sync.Mutex和map写操作 - 避免在压测循环里调
fmt.Sprintf或拼接字符串,改用strings.Builder或预分配[]byte
b.RunParallel 的并发数可控。这两处一错,整个 benchmark 数据就失去横向对比价值。











