并行运行基准测试,即模拟同时请求。

6
当测试从API调用的数据库过程时,当它按顺序运行时,似乎在约3秒内始终保持一致。但是我们注意到,当同时有多个请求时,这可能需要更长时间,导致超时。我正在尝试将“同时发出多个请求”的情况作为go test进行复现。
我尝试了-parallel 10 go test标志,但时间仍然相同,约为28秒。
我的基准测试函数有问题吗?
func Benchmark_RealCreate(b *testing.B) {
    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        name := randomdata.SillyName()
        r := gofight.New()
        u := []unit{unit{MefeUnitID: name, MefeCreatorUserID: "user", BzfeCreatorUserID: 55, ClassificationID: 2, UnitName: name, UnitDescriptionDetails: "Up on the hills and testing"}}
        uJSON, _ := json.Marshal(u)
        r.POST("/create").
            SetBody(string(uJSON)).
            Run(h.BasicEngine(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
                assert.Contains(b, r.Body.String(), name)
                assert.Equal(b, http.StatusOK, r.Code)
            })
    }
}

否则我怎么能达到我想要的目标?

我会质疑这个方法。它看起来像是你想要实现一个部署的负载测试,而不是测试单个代码单元的效率(这已经确定了)。对于后者,Go的基准测试功能是合适的。对于前者,我强烈建议使用类似Apache JMeter这样的暂存环境和工具。 - Markus W Mahlberg
5个回答

6

我刚接触Go语言,但你为什么不尝试编写一个函数并使用标准并行测试运行它呢?

func Benchmark_YourFunc(b *testing.B) {
    b.RunParralel(func(pb *testing.PB) {
        for pb.Next() {
            YourFunc(staff ...T)
        }
    })
}

6
-parallel标志不是用于在多个实例中并行运行相同的测试或基准测试。
引用自命令go:测试标志:
-parallel n
    Allow parallel execution of test functions that call t.Parallel.
    The value of this flag is the maximum number of tests to run
    simultaneously; by default, it is set to the value of GOMAXPROCS.
    Note that -parallel only applies within a single test binary.
    The 'go test' command may run tests for different packages
    in parallel as well, according to the setting of the -p flag
    (see 'go help build').
基本上,如果你的测试允许,你可以使用-parallel来并行运行多个不同的测试或基准函数,但不能在多个实例中同时运行相同的函数。
一般来说,同时运行多个基准函数会破坏基准函数的目的,因为将其在多个实例中并行运行通常会扭曲基准测试的结果。
然而,在你的情况下,你想要测试的不是代码效率,而是一个外部服务。所以Go内置的测试和基准测试设施并不太适合。
当然,我们仍然可以利用这个"基准测试"在我们的其他测试和基准测试运行时自动运行,但你不应该强迫它进入传统的基准测试框架。
第一个想到的方法是使用一个for循环来启动 n个goroutine,它们都尝试调用可测试的服务。其中一个问题是,这只能确保开始时有n个并发的goroutine,因为随着调用开始完成,剩余的并发量将越来越少。
为了解决这个问题并真正测试n个并发调用,你应该有一个拥有n个工作者的工作池,并持续向该工作池提供作业,确保始终有n个并发的服务调用。关于工作池实现,请参见Is this an idiomatic worker thread pool in Go?
因此,总体来说,启动一个拥有n个工作者的工作池,让一个goroutine在任意时间(例如30秒或1分钟)内向它发送作业,并测量(计算)完成的作业数。基准测试结果将是一个简单的除法。
还要注意,仅为了测试目的,可能甚至不需要一个工作池。你可以只使用一个循环来启动n个goroutine,但确保每个启动的goroutine保持调用服务并在单次调用后不返回。

2
你的示例代码混合了几个东西。为什么在那里使用assert?这不是一个测试,而是一个基准测试。如果assert方法很慢,你的基准测试也会很慢。
你还将并行执行从你的代码中移出,放到了测试命令中。你应该尝试使用并发来进行并行请求。这里只是一个开始的可能性:
func executeRoutines(routines int) {
    wg := &sync.WaitGroup{}
    wg.Add(routines)
    starter := make(chan struct{})
    for i := 0; i < routines; i++ {
        go func() {
            <-starter
            // your request here
            wg.Done()
        }()
    }
    close(starter)
    wg.Wait()
}

https://play.golang.org/p/ZFjUodniDHr

在这里我们启动了一些 goroutine,它们会等待直到 starter 关闭。因此,您可以在该行之后直接设置请求。为了让函数等待所有请求完成,我们使用了一个 WaitGroup。

但是重要的是:Go 只支持并发。因此,如果您的系统没有 10 个核心,那么这 10 个 goroutine 将不会并行运行。因此,请确保您有足够的可用核心。

通过这个开始,您可以玩一下。您可以在基准测试中调用此函数。您还可以尝试不同数量的 goroutine。


assert 的作用只是为了确保“基准测试”正确运行。我知道 Go 的并发特性,但我不确定它如何与我的 main_test.go 集成。 - hendry

1

正如文档所示,parallel标志允许同时运行多个不同的测试。通常情况下,您不希望并行运行基准测试,因为这会同时运行不同的基准测试,从而使所有基准测试的结果失真。如果您想对并行流量进行基准测试,则需要将并行流量生成编写到测试中。您需要决定如何使用b.N来完成工作,这是您的工作因子;我可能会将其用作总请求计数,并编写一个或多个基准测试以测试不同的并发负载水平,例如:

func Benchmark_RealCreate(b *testing.B) {
    concurrencyLevels := []int{5, 10, 20, 50}
    for _, clients := range concurrencyLevels {
        b.Run(fmt.Sprintf("%d_clients", clients), func(b *testing.B) {
            sem := make(chan struct{}, clients)
            wg := sync.WaitGroup{}
            for n := 0; n < b.N; n++ {
                wg.Add(1)
                go func() {
                    name := randomdata.SillyName()
                    r := gofight.New()
                    u := []unit{unit{MefeUnitID: name, MefeCreatorUserID: "user", BzfeCreatorUserID: 55, ClassificationID: 2, UnitName: name, UnitDescriptionDetails: "Up on the hills and testing"}}
                    uJSON, _ := json.Marshal(u)
                    sem <- struct{}{}
                    r.POST("/create").
                        SetBody(string(uJSON)).
                        Run(h.BasicEngine(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {})
                    <-sem
                    wg.Done()
                }()
            }
            wg.Wait()
        })
    }
}

注意这里我删除了最初的ResetTimer; 计时器直到调用基准函数才开始,因此在函数中将其作为第一个操作调用是无意义的。它适用于在基准循环之前具有耗时设置的情况,您不希望将其包含在基准结果中。我还删除了断言,因为这是一个基准测试,而不是一个测试; 断言是用于测试中的有效性检查,只会在基准测试中扰乱计时结果。


1
谢谢Adrian!在我的编辑器中,这段代码片段出现了“函数必须在go语句中调用”的错误提示。https://s.natalian.org/2019-04-26/1556258008_2560x1440.png - hendry
1
是的,抱歉,这是我的笔误。go func 缺少了它的 () - Adrian
1
你说得对。在我将其更改为循环遍历一系列并发级别之前,它就已经放置好了...已修复! - Adrian
1
有几种方法可以实现并发流量,所展示的是最简单的方法,但不一定是最有效的(它会生成很多goroutine在信号量上等待)。您可以通过使用工作池来优化它,以减少大量b.N值的内存使用。 - Adrian

1
一件事是基准测试(测量代码运行所需的时间),另一件事是负载/压力测试。
如上所述,-parallel标志允许一组测试并行执行,从而使测试集更快地执行,而不是并行执行某个测试N次。
但是很容易实现你想要的(执行相同的测试N次)。下面是一个非常简单(真正快速且肮脏)的示例,只是为了澄清/演示重要点,以完成这种非常特定的情况:
- 您定义一个测试并将其标记为并行执行 => TestAverage调用t.Parallel - 然后您定义另一个测试并使用RunParallel来执行您想要的测试实例数量(TestAverage)。
测试类:
package math

import (
    "fmt"
    "time"
)

func Average(xs []float64) float64 {
  total := float64(0)
  for _, x := range xs {
    total += x
  }

  fmt.Printf("Current Unix Time: %v\n", time.Now().Unix())
  time.Sleep(10 * time.Second)
  fmt.Printf("Current Unix Time: %v\n", time.Now().Unix())

  return total / float64(len(xs))
}

测试函数:

package math

import "testing"

func TestAverage(t *testing.T) {
  t.Parallel()
  var v float64
  v = Average([]float64{1,2})
  if v != 1.5 {
    t.Error("Expected 1.5, got ", v)
  }
}

func TestTeardownParallel(t *testing.T) {
    // This Run will not return until the parallel tests finish.
    t.Run("group", func(t *testing.T) {
        t.Run("Test1", TestAverage)
        t.Run("Test2", TestAverage)
        t.Run("Test3", TestAverage)
    })
    // <tear-down code>
}

然后只需执行go test,您应该会看到:
X:\>go test
Current Unix Time: 1556717363
Current Unix Time: 1556717363
Current Unix Time: 1556717363

而在此之后的10秒钟。
...
Current Unix Time: 1556717373
Current Unix Time: 1556717373
Current Unix Time: 1556717373
Current Unix Time: 1556717373
Current Unix Time: 1556717383
PASS
ok      _/X_/y        20.259s

最后两行是因为也执行了TestAverage函数。

有趣的是:如果从TestAverage中删除t.Parallel(),则所有内容将按顺序执行:

X:> go test
Current Unix Time: 1556717564
Current Unix Time: 1556717574
Current Unix Time: 1556717574
Current Unix Time: 1556717584
Current Unix Time: 1556717584
Current Unix Time: 1556717594
Current Unix Time: 1556717594
Current Unix Time: 1556717604
PASS
ok      _/X_/y        40.270s

当然,这可以变得更加复杂和可扩展...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接