Points Clés
- Les outils de performance avec une interface graphique peuvent être problématiques dans le cadre d'une utilisation à long terme.
- La lisibilité et la composabilité sont les clés des tests de performance à long terme.
- Vérifiez toujours comment les mesures de performance sont mises en œuvre sous le capot.
- Les tests de performance en tant que code sont une alternative prometteuse à l'approche basée sur une interface graphique.
JMeter et Gatling sont deux des outils de tests de performance les plus populaires. Il y a déjà beaucoup de contenu comparant ces deux projets - alors pourquoi écrire un autre article à ce sujet ? Je vais essayer de comparer les deux outils sous un angle légèrement différent. J'utilise les deux depuis un certain temps et je pense qu'il est temps de résumer mes expériences.
À quoi devraient ressembler les tests de performance dans un monde parfait ?
De mon point de vue de développeur, tester les performances d'une application relève de la responsabilité du développeur qui l'a construite. Ainsi, une approche dans laquelle une équipe dédiée de testeurs n'ayant jamais touché au code source de l'application et devant vérifier ses performances - ne fonctionnera pas. Je ne veux pas dire que les testeurs ne peuvent pas créer de bons tests de performance. Je tiens plutôt à souligner que si nous avons des testeurs de performances à notre disposition, le processus de test des applications doit être mené en étroite collaboration entre les testeurs et les développeurs, surtout au tout début.
Au fil du temps, de plus en plus de responsabilités peuvent être transférées à l'équipe de test, mais les développeurs seront toujours nécessaires pour analyser les résultats. Cela est particulièrement vrai dans une situation où les résultats sont différents de ce à quoi nous nous attendions à l'origine. Bien sûr, c'est très bien pour les développeurs car cela crée une boucle de rétroaction sur la qualité de leurs solutions, de la même manière que dans le cas d'écriture des tests unitaires, d'intégration ou de bout en bout (E2E).
Les tests de performances sont sans aucun doute parmi les tests les plus coûteux en développement logiciel. En plus du temps précieux du testeur ou du développeur pour les créer, ils nécessitent également un environnement dédié (si possible) pour effectuer de tels tests. Les applications changent de manière très dynamique, et les tests de performance doivent donc également suivre ces changements et être tenus à jour. C'est parfois même plus cher que la création originale de tels tests.
Pour les raisons ci-dessus, mon premier conseil en matière de tests de performance est de penser à l'ensemble du processus dans un contexte à long terme. Idéalement, les tests de performance devraient faire partie de notre cycle de déploiement continu (CD). Ce n'est pas toujours possible et n'a pas toujours de sens. Cependant, d'après mes observations, il semble que prendre des raccourcis au début du jeu avec des tests de performances peut nous coûter cher à l'avenir.
Comment choisir des outils pour les tests de performance ?
Choisir un outil de test de performance sera l'un des premiers dilemmes et j'espère que cet article vous aidera à faire le bon choix.
Encore une fois, du point de vue d'un développeur, vous devez vous attendre à quatre attributs principaux d'un bon outil de test des performances :
- lisibilité,
- composabilité,
- mathématiques correctes,
- génération de charge distribuée.
Vous pouvez demander, "C'est ça ?" Pour être honnête - oui. Ce sont les aspects les plus importants lors du choix d'un outil. Bien sûr, il existe de nombreuses fonctionnalités spécifiques et utiles, bien que la plupart des outils offrent un ensemble d'options plus ou moins similaires lorsqu'il s'agit de créer des scénarios de test, de simuler le trafic de production, etc. C'est pourquoi je veux me concentrer sur ces points pour montrer ce qui est vraiment important lors du choix d'un outil qui sera à la fois pratique et maintenable.
Si un outil répond à ces quatre exigences de base, ce n'est qu'alors que nous pourrons procéder à l'analyse de l'ensemble de ses fonctionnalités. Sinon, tenté par une fonction intéressante qui, à la longue, ne sera ni si intéressante ni si utile, nous nous retrouverons avec un outil de test sous-optimal.
La lisibilité des tests
Bon, commençons par le premier point. Comparer la lisibilité de l'application avec l'interface graphique par rapport au code source est une approche plutôt inhabituelle, mais voyons ce qui en ressort. Un flux métier très simple dans JMeter peut ressembler à ceci :
À première vue - assez bon. Tout est clair et il est facile de comprendre ce qu'un test donné teste. Malheureusement, si nous commençons à étendre notre scénario pour ajouter de nouvelles étapes, paramétrer les étapes actuelles ou modifier leur comportement, alors très vite, nous arriverons à la conclusion que c'est un travail très fastidieux. Vous devez beaucoup utiliser la souris, savoir ce qui est caché où et ce qui est pire, vous souvenir de toutes les connexions implicites (par exemple, les variables partagées) entre les requêtes individuelles.
D'après mon expérience, tôt ou tard, l'édition depuis l'interface graphique cessera d'être agréable et nous passerons au code source qui est en XML et (typiquement pour XML) est complètement illisible. En XML, nous pouvons au moins utiliser des techniques de base de refactoring basées sur des chaînes, telles que "remplacer par", etc.
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.3"></jmeterTestPlan>
<hashTree></hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true"></TestPlan>
<stringProp name="TestPlan.comments"></stringProp>
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.tearDown_on_shutdown">True</boolProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
<elementProp name="TestPlan.user_defined_variables" elementtype="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true"></elementProp>
<collectionProp name="Arguments.arguments"></collectionProp>
</elementProp>
<stringProp name="TestPlan.user_define_classpath"></stringProp>
...
That's a very, very long XML.
...
Le code XML complet est disponible ici.
Le même scénario, à Gatling, ressemble à ceci :
val scn =
scenario("Example scenario")
.exec(http("go to main page").get("/"))
.exec(http("find computer").get("/computers?f=macbook"))
.exec(http("edit computer").get("/computers/6"))
.exec(http("go to main page").get("/"))
.repeat(4, "page") {
exec(http("go to page").get("/computers?p=${page}"))
}
.exec(http("go to create new computer page").get("/computers/new"))
.exec(
http("create new computer")
.post("/computers")
.formParam("name", "Beautiful Computer")
.formParam("introduced", "2012-05-30")
.formParam("discontinued", "")
.formParam("company", "37")
)
C'est très similaire à JMeter, c'est-à-dire que vous pouvez voir ce qui est testé et quel est le flux global. N'oublions pas qu'il s'agit cependant d'un code source lisible presque comme des phrases normales. Le Saint Graal de tous les programmeurs. Ce qui doit immédiatement venir à l'esprit, c'est que si nous avons affaire à du code source, nous pouvons utiliser toutes les méthodes de refactoring connues pour étendre le scénario ou améliorer sa lisibilité.
Sans oublier que Scala (utilisé dans Gatling) est un langage hautement typé, donc la plupart des problèmes avec la construction correcte des scénarios seront pris déjà au stade de la compilation du code. Dans JMeter, les erreurs n'apparaîtront qu'au lancement du scénario, ce qui ralentit définitivement la boucle de rétroaction avec les résultats.
Un autre argument en faveur du code source est qu'il est très facile de versionner ces tests et de les faire réviser par de nombreux programmeurs, même issus d'équipes différentes. Bonne chance si vous devez le faire avec des milliers de lignes XML.
La composabilité
La composabilité est cruciale si nous devons créer plusieurs tests de performance qui partagent une certaine logique, par ex. l'authentification, la création d'utilisateurs, etc. Dans JMeter, nous pouvons créer un désastre de copier-coller très rapidement. Même dans ce plan de test simple, nous avons des fragments qui se répètent :
Au fil du temps, il y aura plus de tels endroits. Non seulement les demandes individuelles, mais des sections entières de la logique métier seront dupliquées. Vous pouvez résoudre ce problème en utilisant le Module Controller, ou en créant votre propre extensions dans Groovy ou BeanShell. D'après mon expérience, c'est assez peu pratique et sujet aux erreurs.
Dans Gatling, la création de fragments réutilisables n'est fondamentalement limitée que par nos compétences en programmation. La première étape peut être d'extraire certaines méthodes afin qu'elles puissent être utilisées plusieurs fois.
private val goToMainPage = http("go to main page").get("/")
private def findComputer(name: String) = http("find computer").get(s"/computers?f=${name}")
private def editComputer(id: Int) = http("edit computer").get(s"/computers/${id}")
private def goToPage(page: Int) = http("go to page").get(s"/computers?p=${page}")
private val goToCreateNewComputerPage = http("go to create new computer page").get("/computers/new")
private def createNewComputer(name: String) =
http("create new computer")
.post("/computers")
.formParam("name", name)
.formParam("introduced", "2012-05-30")
.formParam("discontinued", "")
.formParam("company", "37")
val scn =
scenario("Example scenario")
.exec(goToMainPage)
.exec(findComputer("macbook"))
.exec(editComputer(6))
.exec(goToMainPage)
.exec(goToPage(1))
.exec(goToPage(1))
.exec(goToPage(3))
.exec(goToPage(10))
.exec(goToCreateNewComputerPage)
.exec(createNewComputer("Awesome computer"))
Ensuite, nous pouvons diviser notre scénario en fragments plus petits, puis combiner et créer un flux métier plus compliqué.
val search = exec(goToMainPage)
.exec(findComputer("macbook"))
.exec(editComputer(6))
val jumpBetweenPages = exec(goToPage(1))
.exec(goToPage(1))
.exec(goToPage(3))
.exec(goToPage(10))
val addComputer = exec(goToMainPage)
.exec(goToCreateNewComputerPage)
.exec(createNewComputer("Awesome computer"))
val scn =
scenario("Example scenario")
.exec(search, jumpBetweenPages, addComputer)
Si nous devons maintenir des tests de performance à long terme, alors un haut niveau de composabilité sera sans aucun doute un avantage par rapport aux autres outils. D'après mes observations, il ressort que seuls les outils permettant d'écrire des tests dans le code source, par ex. Gatling et Scala, Locust et Python, WRK2 et Lua, répondent à ce critère. Si les tests sont enregistrés sous forme de formats texte tels que XML, JSON, etc., nous serons toujours limités par la composabilité de ces formats.
Des mathématiques correctes
Il y a un dicton que tout testeur de performance devrait connaître : "des mensonges, des foutus mensonges et des statistiques". S'ils ne le savent pas encore, ils l'apprendront sûrement de manière douloureuse. Un article séparé pourrait être écrit sur la raison pour laquelle cette phrase devrait être le mantra dans le domaine des tests de performance. En un mot : la médiane, la moyenne arithmétique, l'écart type sont des mesures complètement inutiles dans ce domaine (vous ne pouvez les utiliser que comme un aperçu supplémentaire). Vous pouvez obtenir plus de détails à ce sujet dans cette superbe présentation de Gil Tene, CTO et co-fondateur chez Azul. Ainsi, si l'outil de test de performance ne fournit que ces données statiques, elles peuvent être rejetées immédiatement.
Les seuls indicateurs significatifs pour mesurer et comparer les performances sont les percentiles. Cependant, vous devez également les utiliser avec une certaine méfiance quant à la manière dont ils ont été mis en œuvre. Très souvent, la mise en œuvre est basée sur la moyenne arithmétique et l'écart type, ce qui, bien sûr, les rend tout aussi inutiles.
À partir de la présentation ci-dessus, vous pouvez apprendre à vérifier l'exactitude des percentiles.
Une autre approche consisterait à vérifier vous-même le code source de l'implémentation. Je regrette que la plupart de la documentation des outils de test de performance ne couvre pas la façon dont les percentiles sont calculés. Même si une telle documentation existe, peu de gens l'utiliseront et peuvent donc tomber dans certains pièges, par ex. Mise en œuvre des métriques dans Dropwizard.
Sans mathématiques/statistiques correctes, tout notre travail dans le cadre des tests de performance peut être complètement sans valeur, car nous ne pourrons pas comprendre les résultats d'un seul test ou comparer les résultats entre eux.
Dans mes tests, je m'appuie très souvent sur des graphiques avec des percentiles évoluant dans le temps, ce qu'il est possible d'obtenir à la fois dans Gatling et JMeter. Grâce à cela, nous sommes en mesure de dire si le système testé n'a pas de problèmes de performances pendant tout le test.
Pour comparer les résultats de tests individuels, vous avez besoin de percentiles globaux (disponibles dans les deux outils). Cependant, j'ai une fois rebondi sur un problème assez intéressant avec la précision des percentiles globaux dans JMeter. Gatling, dans son implémentation, utilise la bibliothèque HdrHistogram pour calculer les percentiles, qui offre un compromis très raisonnable entre précision et besoins en mémoire.
Les tests distribués
Il existe des articles sur les performances des outils de test de performance. Cela peut être important jusqu'à un certain niveau, car on veut sans doute générer un énorme trafic pour bien "pousser" le système testé. Le problème est que les applications actuelles sont très rarement des instances uniques exécutées sur une seule machine. Nous avons affaire à des systèmes distribués fonctionnant sur de nombreuses instances et de nombreuses machines (souvent dans des solutions cloud dynamiquement évolutives). Une seule machine exécutant un test de performances ne pourra pas générer suffisamment de charge pour tester un tel environnement. Par conséquent, au lieu de se concentrer sur l'outil qui générera le plus de trafic à partir d'une seule machine, il est préférable de vérifier s'il existe une option pour exécuter des tests distribués à partir de plusieurs machines, en même temps.
Dans ce cas, nous avons un match nul. Nous pouvons disperser nos tests manuellement dans Gatling ainsi que dans JMeter. De plus, nous pouvons utiliser des solutions existantes qui le feront pour nous automatiquement comme Flood ou Gatling Entreprise. Je recommande définitivement la deuxième option, car elle nous fera gagner un temps précieux.
Conclusion
Bien que le ton général de cet article puisse être perçu comme un tir groupé sur JMeter, ce n'était pas mon intention car j'ai utilisé ces deux outils. J'avais l'habitude de voir JMeter comme le seul outil sensé pour tester les performances, mais quand j'ai commencé à utiliser Gatling, je ne voyais plus l'intérêt de revenir à JMeter.
Un outil avec une interface graphique sera probablement plus facile à utiliser au tout début, mais l'idée d'un test de performance sous forme de code m'attire définitivement plus. Sans oublier que le DSL de Gatling est vraiment agréable et pratique à utiliser. Les tests sont lisibles et beaucoup plus faciles à maintenir.
Beaucoup de gens sont sceptiques à propos de Gatling car cela nécessite l'apprentissage d'un nouveau langage de programmation - Scala, qui a été perçu comme un langage difficile et difficile à utiliser. Rien ne pourrait être plus éloigné de la vérité, cependant. Le langage a ses avantages et ses inconvénients, alors que dans le contexte de Gatling, seule une connaissance de base de la syntaxe est requise. Si, d'un autre côté, vous avez toujours voulu utiliser Scala dans votre travail, mais que cela n'a pas été possible pour diverses raisons, peut-être que des tests de performance (et automatiques) seront un bon moyen d'introduire doucement ce langage dans votre écosystème. Sachez que depuis Gatling 3.7 vous pouvez l'utiliser avec Java ! Ce sera le sujet de mon prochain article. Restez à l'écoute.