golang 压力测试与并发安全测试

2022-06-27 14:16:11 浏览数 (1)

1. 引言

上一篇文章中,介绍了如何通过 go test 实现单元测试: 测试驱动开发与 golang 单元测试

但单元测试只是 go test 最为基础的用法,本文就来介绍 go test 更为进阶的基准测试和并发安全测试。

2. 基准测试 — benchmark test

很多时候,我们不仅需要测试程序执行的正确性,对于程序执行的性能消耗我们往往更加看重,毕竟在项目上线前,究竟需要多少资源来部署项目,项目能够承受多大的流量,不对这些了然于胸,就无法保证线上业务的安全,后果将会是灾难性的。 go test 工具同样也提供了压力测试等功能的支持 — benchmark test。

2.1. 基准测试的编写与执行

go test 的基准测试提供了将目标代码段执行 N 次统计运行时间,从而实现压测的功能。 与单元测试类似,只要在项目的 xxx_test.go 文件中写入下面的方法即可实现基准测试函数的编写:

代码语言:javascript复制
func BenchmarkXxx(*testing.B) {
// 测试函数体
}

例如:

代码语言:javascript复制
func BenchmarkFib10(b *testing.B) {
        for n := 0; n < b.N; n   {
                Fib(10)
        }
}

执行:

go test -bench=.

就会展示:

$ go test -bench=. BenchmarkFib10-4 3000000 424 ns/op PASS ok chapter09/testing 1.724s

表示执行了 3000000 次,耗时 1.724 秒,每次调用 424 纳秒。

2.2. 设定执行时间

我们也可以通过 -benchtime 来指定测试的执行时间:

$ go test -bench=Fib40 -benchtime=20s BenchmarkFib40-4 30 838675800 ns/op

3. testing.B

代码语言:javascript复制
type B struct {
    common
    importPath       string // import path of the package containing the benchmark
    context          *benchContext
    N                int
    previousN        int           // number of iterations in the previous run
    previousDuration time.Duration // total duration of the previous run
    benchFunc        func(b *B)
    benchTime        benchTimeFlag
    bytes            int64
    missingBytes     bool // one of the subbenchmarks does not have bytes set.
    timerOn          bool
    showAllocResult  bool
    result           BenchmarkResult
    parallelism      int // RunParallel creates parallelism*GOMAXPROCS goroutines
    // The initial states of memStats.Mallocs and memStats.TotalAlloc.
    startAllocs uint64
    startBytes  uint64
    // The net total of this test after being run.
    netAllocs uint64
    netBytes  uint64
    // Extra metrics collected by ReportMetric.
    extra map[string]float64
}

可以看到,testing.B 的首个元素也是 common 结构,因此他也拥有 testing.T 中所有的报告方法,可以参考上一篇文章的讲解: 测试驱动开发与 golang 单元测试

4. 并行测试

既然是性能压测,串行的执行统计运行耗时常常并不是我们想要的测试手段,通过并发执行来观察资源的消耗情况是更好的测试方法。 go test 提供了 b.RunParallel 方法用来实现让多个基准测试方法并行执行的功能。

代码语言:javascript复制
func BenchmarkTemplateParallel(b *testing.B) {
    templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
    b.RunParallel(func(pb *testing.PB) {
         // 每个 goroutine 有属于自己的 bytes.Buffer.
        var buf bytes.Buffer
        for pb.Next() {
              // 所有 goroutine 一起,循环一共执行 b.N 次
            buf.Reset()
            templ.Execute(&buf, "World")
        }
    })
}

调用了 b.RunParallel 方法的测试函数将会在单独的 goroutine 中启动。 需要注意的是,b.StartTimer、b.StopTime、b.ResetTimer 三个方法会影响到所有 goroutine,因此不要在并行测试中调用。

4.1. 调节并发度

并行基准测试其并发度受环境变量 GOMAXPROCS 控制,默认情况下是 CPU 核心数。 可以在测试开始前,通过 b.SetParallelism 方法实现对并发度的控制,例如执行b.SetParallelism(2) 则意味着并发度为 2*GOMAXPROCS。 在执行 go test 命令时,增加 -cpu 参数,可以打印 cpu 资源消耗的详情信息。

5. 内存统计

除了观察 CPU 的调用情况,我们也常常需要去观察内存的使用情况。 通过 go test 命令添加 -benchmem 参数可以打开基准测试的内存统计功能。 也可以通过在测试用例执行开始前,调用 b.ReportAllocs 函数,这样做的好处是只会影响你需要的函数:

代码语言:javascript复制
func BenchmarkTmplExucte(b *testing.B) {
    b.ReportAllocs()
    templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
    b.RunParallel(func(pb *testing.PB) {
        // Each goroutine has its own bytes.Buffer.
        var buf bytes.Buffer
        for pb.Next() {
            // The loop body is executed b.N times total across all goroutines.
            buf.Reset()
            templ.Execute(&buf, "World")
        }
    })
}

打印出了:

BenchmarkTmplExucte-4 2000000 898 ns/op 368 B/op 9 allocs/op

表示总计执行 2000000 次,平均每次耗时 898 纳秒,每次执行消耗内存 368 Bytes,共计分配内存 9 次。

6. 并发安全测试 — -race

在介绍 goroutine 并发安全时,我们曾经介绍了并发安全测试相关的内容: goroutine 并发中竞争条件的解决

只要在 go test 命令中加入 -race 参数,就可以在测试阶段发现可能的并发安全问题。 下面是一个典型的非并发安全的例子:

代码语言:javascript复制
func TestParallelSafe(t *testing.T) {
    a := 1
    go func(){
        a = 2
    }()
    a = 3
    t.Logf("a is ", a)

    time.Sleep(2 * time.Second)
}

执行 go test -race . 会打印出:

runtime go test -race . a is 3 ================== WARNING: DATA RACE Write by goroutine 5: main.func·001() /data/test/race1.go:11 0x3a Previous write by main goroutine: main.main() /data/test/race1.go:13 0xe7 Goroutine 5 (running) created at: main.main() /data/test/race1.go:12 0xd7 ================== Found 1 data race(s) exit status 66

从而可以发现竞争条件的存在。 但需要注意的是,只有测试用例覆盖到的代码才可以顺利检测出竞争,因此保证测试用例的覆盖率是一个很重要的事。

6.1. 打印测试用例覆盖率报告

go test 命令增加 -coverprofile 参数,指定输出文件,就可以输出测试的覆盖率报告。

0 人点赞