【问题标题】:Run a benchmark in parallel, i.e. simulate simultaneous requests并行运行基准测试,即模拟同时请求
【发布时间】:2019-04-22 05:07:12
【问题描述】:

在测试从 API 调用的数据库过程时,当它按顺序运行时,它似乎在大约 3 秒内始终如一地运行。但是我们注意到,当多个请求同时进入时,这可能需要更长的时间,从而导致超时。我正在尝试以go test 的形式重现“一次多个请求”的情况。

我尝试了 -parallel 10 去测试标志,但 the timings 在大约 28 秒时是相同的。

我的benchmark function有什么问题吗?

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 的 benchmark 特性是合适的。对于前者,我强烈建议使用暂存环境并使用Apache JMeter

标签: go testing benchmarking


【解决方案1】:

-parallel 标志不适用于在多个实例中并行运行相同的测试或基准测试。

引用Command go: Testing flags:

-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 goroutines,它们都试图调用可测试的服务。这样做的一个问题是,这只能确保 n 在开始时并发 goroutine,因为随着调用开始完成,剩余的并发会越来越少。

要克服这个问题并真正测试n 并发调用,您应该有一个带有n 工作人员的工作人员池,并不断向该工作人员池提供工作,确保始终有n 并发服务调用.有关工作器池的实现,请参阅Is this an idiomatic worker thread pool in Go?

总而言之,用nworkers 启动一个worker pool,让一个goroutine 在任意时间(例如30 秒或1 分钟)向它发送作业,并测量(计数)已完成的作业。基准测试结果将是一个简单的除法。

另请注意,仅出于测试目的,甚至可能不需要工作程序池。您可以只使用循环来启动 n goroutines,但要确保每个启动的 goroutine 都继续调用服务,并且在一次调用后不会返回。

【讨论】:

    【解决方案2】:

    我是新手,但您为什么不尝试创建一个函数并使用标准并行测试运行它?

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

    【讨论】:

      【解决方案3】:

      您的示例代码混合了几件事。你为什么在那里使用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 的数量。

      【讨论】:

      • 断言只是为了确保“基准”运行正确。我知道 Go 的并发功能,但我不确定它如何与我的 main_test.go 集成。
      【解决方案4】:

      如文档所示,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;在调用基准函数之前,计时器不会启动,因此将其作为函数中的第一个操作调用是没有意义的。它适用于您不希望包含在基准结果中的基准循环之前进行耗时设置的情况。我还删除了断言,因为这是一个基准,而不是一个测试;断言用于测试中的有效性检查,仅用于在基准测试中抛出计时结果。

      【讨论】:

      • 谢谢阿德里安!我在编辑器中的此代码 sn-p 上收到“必须在 go 语句中调用函数”。 s.natalian.org/2019-04-26/1556258008_2560x1440.png
      • 是的,抱歉,我的错字。 go func 缺少它的 ()
      • 你是对的。这是在我将其更改为循环一系列并发级别之前放置的......修复!
      • 并发流量有几种方法,显示的是最简单的但不一定是最有效的(它会生成大量等待信号量的goroutines)。您可以使用工作池对其进行优化,以减少 b.N 的较大值的内存使用量。
      【解决方案5】:

      一件事是基准测试(测量代码运行所需的时间),另一件事是负载/压力测试。

      上面所说的-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 测试,你应该会看到:

      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
      

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-08-14
        • 1970-01-01
        • 2020-03-30
        • 2010-09-07
        • 2021-12-04
        • 2022-01-20
        • 1970-01-01
        相关资源
        最近更新 更多