5
votes

Exécuter un benchmark en parallèle, c'est-à-dire simuler des requêtes simultanées

Lors du test d'une procédure de base de données invoquée depuis une API, lorsqu'elle s'exécute séquentiellement, elle semble s'exécuter de manière cohérente en ~ 3s. Cependant, nous avons remarqué que lorsque plusieurs demandes arrivent en même temps , cela peut prendre beaucoup plus de temps, ce qui entraîne des délais d'attente. J'essaye de reproduire le cas "plusieurs requêtes à la fois" comme un go test .

J'ai essayé le flag test go -parallel 10 , mais les horaires étaient les mêmes à ~ 28 s.

Y a-t-il un problème avec ma fonction de référence ?

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)
            })
    }
}

Sinon, comment puis-je réaliser ce que je recherche?


1 commentaires

Je remettrais en question l'approche. Cela se lit comme si vous souhaitiez réaliser un test de charge d'un déploiement, plutôt que de tester l'efficacité d'une unité de code individuelle (que vous avez déjà établie). Pour ce dernier, la fonction de référence de Go convient. Pour le premier, je suggérerais fortement un environnement de préparation et l'utilisation de quelque chose comme Apache JMeter


5 Réponses :


2
votes

Votre exemple de code mélange plusieurs choses. Pourquoi utilisez-vous assert là-bas? Ce n'est pas un test, c'est une référence. Si les méthodes assert sont lentes, votre benchmark le sera.

Vous avez également déplacé l'exécution parallèle de votre code vers la commande de test. Vous devriez essayer de faire une demande parallèle en utilisant la concurrence. Voici juste une possibilité comment commencer:

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

Nous commençons quelques goroutines ici, qui attendent que starter soit fermé. Vous pouvez donc définir votre demande directement après cette ligne. Que la fonction attend que toutes les requêtes soient terminées, nous utilisons un WaitGroup.

MAIS IMPORTANT: Go ne prend en charge que la concurrence. Donc, si votre système n'a pas 10 cœurs, les 10 goroutines ne fonctionneront pas en parallèle. Assurez-vous donc d'avoir suffisamment de cœurs disponibles.

Avec ce début, vous pouvez jouer un peu. Vous pouvez commencer à appeler cette fonction dans votre benchmark. Vous pouvez également jouer avec le nombre de goroutines.


1 commentaires

L'affirmation est juste de s'assurer que le "benchmark" a fonctionné correctement. Je connais la fonctionnalité d'accès concurrentiel de Go, mais je ne sais pas comment cela s'intègre à mon main_test.go .



5
votes

L'indicateur -parallel n'est pas pour exécuter le même test ou le même benchmark en parallèle, dans plusieurs instances.

Citation de Commande go: Test des indicateurs:

-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').

Donc, fondamentalement, si vos tests le permettent, vous pouvez utiliser -parallel pour exécuter plusieurs fonctions de test ou de benchmark distinctes en parallèle, mais pas la même dans plusieurs instances.

En général, exécuter plusieurs fonctions de référence en parallèle va à l'encontre de l'objectif de l'analyse comparative d'une fonction, car son exécution en parallèle dans plusieurs instances déforme généralement l'analyse comparative.

Cependant, dans votre cas, l'efficacité du code n'est pas ce que vous voulez mesure, vous souhaitez mesurer un service externe. Donc, les installations de test et de benchmarking intégrées de go ne sont pas vraiment adaptées.

Bien sûr, nous pourrions toujours utiliser la commodité de faire exécuter automatiquement ce "benchmark" lorsque nos autres tests et benchmarks sont exécutés, mais vous ne devriez pas forcer ceci dans le cadre de benchmarking conventionnel.

La première chose qui vient à l'esprit est d'utiliser une boucle for pour lancer n goroutines qui tentent toutes d'appeler le service testable. Un problème avec ceci est que cela garantit seulement n goroutines simultanées au début, car à mesure que les appels commencent à se terminer, il y aura de moins en moins de concurrence pour les autres.

Pour surmonter ce problème et tester réellement n appels simultanés, vous devez disposer d'un pool de nœuds de calcul avec n nœuds de calcul et alimenter en continu les travaux à ce pool de nœuds de calcul, en vous assurant qu'il y aura n appels de service simultanés à tout moment. Pour une implémentation de pool de nœuds de calcul, voir Est-ce un pool de threads de travail idiomatique dans Go?

Donc, dans l'ensemble, lancez un pool de travailleurs avec n nœuds de calcul, demandez à un goroutine de lui envoyer des tâches pendant une durée arbitraire (par exemple pendant 30 secondes ou 1 minute), et mesurez (comptez) les travaux terminés. Le résultat du benchmark sera une simple division.

Notez également qu'à des fins de test uniquement, un pool de nœuds de calcul peut même ne pas être nécessaire. Vous pouvez simplement utiliser une boucle pour lancer n goroutines, mais assurez-vous que chaque goroutine démarrée continue d'appeler le service et ne revient pas après un seul appel.


0 commentaires

1
votes

Comme l'indique la documentation, l'indicateur parallel permet d'exécuter plusieurs tests différents en parallèle. En règle générale, vous ne voulez pas exécuter des benchmarks en parallèle, car cela exécuterait différents benchmarks en même temps, rejetant les résultats pour tous. Si vous souhaitez évaluer le trafic parallèle, vous devez écrire la génération de trafic parallèle dans votre test. Vous devez décider comment cela doit fonctionner avec b.N qui est votre facteur de travail; Je l'utiliserais probablement comme le nombre total de demandes et j'écrirais un ou plusieurs tests de performance testant différents niveaux de charge simultanés, par exemple:

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()
        })
    }
}

Notez ici que j'ai supprimé le code initial ResetTimer >; le minuteur ne démarre pas tant que votre fonction de référence n'est pas appelée, donc l'appeler comme la première opération de votre fonction est inutile. Il est destiné aux cas où vous avez une configuration longue avant la boucle de référence que vous ne voulez pas inclure dans les résultats de référence. J'ai également supprimé les affirmations, car il s'agit d'un point de repère, pas d'un test; les affirmations servent à vérifier la validité des tests et ne servent qu'à rejeter les résultats de chronométrage dans les benchmarks.


4 commentaires

Merci Adrian! J'obtiens une "fonction doit être invoquée dans l'instruction go" sur cet extrait de code dans mon éditeur. s.natalian.org/2019-04-26/1556258008_2560x1440.png


Ouais, excuses, faute de frappe de ma part. le go func manquait son () .


Vous avez raison. Cela a été placé avant que je ne le modifie en boucle sur une série de niveaux de concurrence ... corrigé!


Il y a plusieurs façons de faire le trafic concurrent, ce qui est montré est le plus simple mais pas forcément le plus efficace (cela générera beaucoup de goroutines assis en attente sur le sémaphore). Vous pouvez l'optimiser avec un pool de nœuds de calcul pour réduire l'utilisation de la mémoire pour les grandes valeurs de b.N .



2
votes

Je suis nouveau, mais pourquoi n'essayez-vous pas de créer une fonction et de l'exécuter en utilisant le test parallèle standard?

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


0 commentaires

0
votes

Une chose est l'analyse comparative (mesurer le temps que le code prend pour s'exécuter) une autre est les tests de charge / contrainte.

L'indicateur -parallel comme indiqué ci-dessus, est de permettre à un ensemble de tests de s'exécuter en parallèle, permettant ainsi à l'ensemble de tests pour s'exécuter plus vite, ne pas exécuter certains tests N fois en parallèle.

Mais c'est simple pour réaliser ce que l'on veut (exécution du même test N fois). Ci-dessous un exemple très simple (vraiment rapide et sale) juste pour clarifier / démontrer les points importants, qui permet de faire cette situation très spécifique:

  • Vous définissez un test et le marquez comme étant exécuté en parallèle => TestAverage avec un appel à t.Parallel
  • Vous définissez ensuite un autre test et utilisez RunParallel pour exécuter le nombre d'instances du test (TestAverage) souhaité.

La classe à tester:

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

Les fonctions de test:

...
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

Ensuite, faites simplement un allez tester et vous devriez voir:

X:\>go test
Current Unix Time: 1556717363
Current Unix Time: 1556717363
Current Unix Time: 1556717363

Et 10 secondes après cela

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>
}

Les deux lignes supplémentaires, à la fin sont parce que TestAverage est également exécuté.

Le point intéressant ici: si vous supprimez t.Parallel () de TestAverage, tout sera exécuté séquentiellement:

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))
}

Cela peut bien sûr être rendu plus complexe et extensible ...


0 commentaires