如何编写高并发基准测试_b.RunParallel并行性能评估

1次阅读

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

如何编写高并发基准测试_b.RunParallel并行性能评估

为什么 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.WaitGroupchan 怎么选?

优先用 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.Mutexmap 写操作
  • 避免在压测循环里调 fmt.Sprintf 或拼接字符串,改用 strings.Builder 或预分配 []byte

实际写的时候,最常漏掉的是把初始化逻辑包进计时范围,或者误以为 b.RunParallel 的并发数可控。这两处一错,整个 benchmark 数据就失去横向对比价值。

text=ZqhQzanResources