Publié par
Il y a 4 années · 11 minutes · Java / JEE

Gatling, ou comment écrouler un serveur – alternative à JMeter

Dans certains projets à fort trafic, les tests de charge sont souvent négligés faute de temps ou bien faute d’outils simples à intégrer au projet. Et ceci à tort, car seuls les tests de charge permettent de valider correctement une application ou un système avant déploiement, tant en qualité de service qu’en consommation de ressources.

Même si Apache JMeter est l’une des références, son interface vieillissante, et sa complexité de mise en œuvre, n’en font pas l’outil idéal et sexy que nous souhaitons tous posséder. Pourtant, une alternative combinant simplicité d’usage, performance, et fiabilité existe.

Par la pratique, cette article vous propose donc la mise en œuvre simple et rapide de tests de charge avec Gatling.

Qu’est-ce que Gatling?

Un peu de culture ne pouvant nuire à notre santé, le projet Gatling tire son nom et son logo de la première mitrailleuse efficace combinant fiabilité, puissance de feu et facilité d’alimentation

Le principe conçu et mis au point par Richard Gatling, en 1861, offre le moyen de paralléliser efficacement les opérations mécaniques nécessaires (chargement, percussion, extraction, éjection) et laisse chambres et canons mieux refroidir, donc atteint des cadences de tir élevées, sans commune mesure avec les armes à un seul canon. (dixit Wikipédia).

Ceci étant dit, on peut s’amuser à faire quelques analogies entre cette mitrailleuse du 19ème siècle et le projet Gatling :

  • fiabilité : développé en Scala et s’exécutant sur la JVM;
  • puissance de feu / parallélisation des opérations : client HTTP asynchrone basé sur Netty et utilisation d’acteurs Akka;
  • facilité d’alimentation : un domaine de langage dédié (DSL) clair et concis.

En plus de cela, Gatling utilise Highcharts pour générer ses graphes, ce qui apporte la touche sexy au projet. Bien sûr le tout est "Open Source", sous licence Apache v2, et disponible sur GitHub, si vous avez envie d’y jeter un œil ou même d’y contribuer.

Installation / Intégration

Gatling est fourni directement via une archive tout-en-un disponible ici. La version utilisée dans notre cas sera la 2.0-M2, avant de la télécharger et de l’utiliser, assurez-vous d’avoir rempli les pré-requis.

Voici la structure de l’archive une fois décompressée :

Capture-d’écran-2013-05-11-à-11.40.36.png

On dispose alors d’une interface en ligne de commande (CLI) permettant d’exécuter une simulation :

Capture-d’écran-2013-05-11-à-11.44.26.png

Par défaut, Gatling fournit 2 simulations disponible dans le répertoire ‘user-files’. On peut alors exécuter l’une d’elle afin de vérifier que tout fonctionne correctement :

Capture-d’écran-2013-05-14-à-13.46.36.png

L’utilisation d’une CLI est très pratique pour faire rapidement quelques tests mais elle a ses limites lorsque l’on travaille sur un vrai projet.

On préfèrera alors utiliser l’une des possibilités d’intégration suivantes :

  • un plugin Maven officiel : fonctionne bien et suffisant pour un grand nombre de projet Java;
  • un ensemble de plugins Tiers : pour Play2!, SBT et Gradle. 

Écrire un premier scénario

On va écrire un premier scénario qui consiste en la consultation d’un ou plusieurs produits sur notre site phare: The Bees Shop.

Le scénario est le suivant :

  • un utilisateur visite le site et arrive sur la page d’accueil;
  • l’utilisateur consulte la liste des produits disponibles;
  • l’utilisateur accède, en moyenne, au détail de 5 produits. 

Pour ce scénario plutôt simple l’interface en ligne de commande sera utilisée.

Une bonne pratique de Gatling est de séparer les simulations et les jeux de données des scénarios. Nous allons donc créer 3 fichiers:

  • un jeu de données ‘products.csv’ qui contiendra la liste des produits;
  • un fichier ‘ConsultProductsScenario.scala‘ qui sera l’implémentation de notre scénario;
  • un fichier ‘BeesShopSimulation.scala‘ qui permettra d’exécuter notre scénario.

 Ce qui donne l’arborescence suivante après création des fichiers :

Capture+d’écran+2013-05-29+à+13.24.24

Le fichier de données ‘products.csv’ contient 2 produits :

  • le Long Island Iced Tea qui est un cocktail à base de tequila, de gin, de vodka, de rhum et de liqueur d’oranges;
  • le Sex On the Beach qui est un cocktail à base de vodka, de schnapps à la pêche, de jus d’orange et de jus de citron.

Bref, ce qui donne concrètement :

productId,productName
1,Long Island Iced tea
2,Sex On The Beach

Même si vous n’êtes pas un "Scalafiste" avéré, la plus grande difficulté de Gatling, à mon sens, ne vient pas de l’apprentissage de Scala mais plutôt de l’apprentissage de son DSL. C’est pourquoi dans cette article je vais essayer de fournir un maximum d’exemple divers et concrets, afin d’enrichir le wiki officiel du projet.

Le scénario ‘ConsultProductsScenario.scala‘ s’écrit donc comme suit :

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import bootstrap._

object ConsultProductsScenario {

  val products = csv("products.csv").random

  val scn = scenario("View 5 random products")
    .exec(
      http("Home page")
        .get("/")
        .check(status.is(200)))
    .exec(
      http("View the list of products")
        .get("/product")
        .check(status.is(200)))
    .repeat(5) {
      feed(products)
      .exec(
        http("View a random product")
          .get("/product/${productId}")
          .check(status.is(200)))
    }
}

Quelques explications tout de même :

  • la variable ‘products‘ est un feeder initialisé avec une stratégie aléatoire;
  • la méthode ‘feed()‘ permet d’injecter notre source de données dans la session de l’utilisateur;
  • l’utilisation d’une EL/product/${productId}’ permet d’accéder directement à une propriété enregistrée dans la session de l’utilisateur. Dans notre cas il s’agit de l’identifiant d’un produit.

Il est aussi possible d’utiliser directement une connexion JDBC en tant que source de données, ce qui pour moi est préférable en terme de cohérence et de maintenance des scénarios. Par exemple, si dans la base de test on supprime un des 2 produits par mégarde alors on risque d’avoir une requête sur deux en échec dû à une 404 ce qui biaise complétement les résultats.

On va donc écrire notre fichier ‘BeesShopSimulation.scala‘, qui sera pour l’exemple un test de montée en charge :

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class BeesShopSimulation extends Simulation {

  val httpConf = httpConfig.baseURL("http://localhost:8085/bees-shop")

  setUp(
    ConsultProductsScenario.scn
      .inject(rampRate(10 usersPerSec) to(100 usersPerSec) during(5 minutes))
      .protocolConfig(httpConf)
  )
}

Vous aurez remarqué que l’on étend la classe Simulation qui va faire office de runner lors du lancement du test. On définit ensuite la configuration HTTP à utiliser :

val httpConf = httpConfig.baseURL("http://localhost:8085/bees-shop")

Cette configuration est plutôt minimaliste dans notre cas, si dans votre projet vous passer par un proxy ou bien par un load balancer, alors je vous renvoie directement au wiki partie configuration du protocole.

Il ne nous reste plus qu’à configurer notre scénario avec une rampe faisant passer le nombre d’utilisateurs simultanées de 10 utilisateurs/s à 100 utilisateurs/s en 5 minutes.

inject(rampRate(10 usersPerSec) to(100 usersPerSec) during(5 minutes))

Exécution de la simulation, via le script fournit par Gatling :

Capture+d’écran+2013-05-29+à+13.27.22

Fin de la simulation :

Capture+d’écran+2013-07-01+à+16.06.27

Les résultats sont disponibles au format HTML, vous pouvez les consulter en ligne ici, et voici le tableau récapitulatif du nombre de requêtes par seconde :

  • en vert: les requêtes OK qui ont passé l’ensemble des check() du scénario
  • en rouge: les requêtes KO qui ont échoué sur au moins un des check() ou si il y’a eu une exception lors de l’exécution (ex: SocketTimeoutException)

Capture+d’écran+2013-07-01+à+16.07.50

Attention: ne pas confondre nombre d’utilisateurs simultanées et nombre de requêtes par seconde, dans notre cas un utilisateur exécute plusieurs requêtes par seconde.

Combiner plusieurs scénarios

On souhaite maintenant réaliser une simulation combinant les 3 scénarios suivants :

  • un client consulte 5 produits aléatoires;
  • un client recherche un produit et écrit un commentaire dessus;
  • un client ajoute un produit sur trois à son panier lorsqu’il le consulte.

Le premier scénario ‘ConsultProductsScenario.scala‘ est déjà fait, voir précédemment.

Le second scénario ‘SearchAndCommentProductsScenario.scala‘ est le suivant :

import io.gatling.core.Predef._
import io.gatling.http.Predef._

object SearchAndCommentProductsScenario {

  val products = csv("products.csv").random

  val scn = scenario("Search a product")
    .exec(
      http("Home page")
        .get("/")
        .check(status.is(200)))
    .feed(products)
    .exec(
      http("Search a random product by name")
        .get("/product/")
        .queryParam("name", "${productName}")
        .check(status.is(200))
        .check(regex("""href=".*/product/(\d+)"""").find.exists.saveAs("productIdFound")))
    .exec(
      http("View a product")
        .get("/product/${productIdFound}")
        .check(status.is(200)))
    .exec(
      http("Comment a product")
        .post("/product/${productIdFound}/comment")
        .param("comment", "My 2 cents!")
        .check(status.is(200)))
}

Pour rechercher un produit on va passer en paramètre de notre GET le nom du produit recherché grâce à la méthode queryParam() :

queryParam("name", "${productName}")

Ensuite on va vérifier que le produit recherché existe bien grâce à une regex et on va utiliser la méthode saveAs() pour sauvegarder son ID dans une variable de session :

check(regex("""href=".*/product/(\d+)"""").find.exists.saveAs("productIdFound")))

On peut alors poster un commentaire sur le produit recherché grâce à la méthode param() :

post("/product/${productIdFound}/comment").param("comment", "My 2 cents!")

Le troisième scénario ‘AddProductsInCartScenario.scala‘, qui demande cette fois quelques notions de Scala, est le suivant :

import io.gatling.core.Predef._
import io.gatling.core.validation.Validation
import io.gatling.http.Predef._
import scala.concurrent.duration._
import bootstrap._

object AddProductsInCartScenario {

  val products = csv("products.csv").random.build
  val numberOfProductsRegex: (Session) => Validation[String] = """(\d+) items"""
  val numberOfProducts: String = "numberOfProductsInCart"

  val scn = {
    scenario("Add 3 products in cart")
      .exec(
        http("Home page")
          .get("/")
          .check(status.is(200))
          .check(regex(numberOfProductsRegex).find.transform(_.map(_.toInt)).is(0).saveAs(numberOfProducts)))
      .feed(products)
      .asLongAs(_.get[Int](numberOfProducts, Int.MaxValue) < 3) {
        exec(
          http("View a product")
            .get("/product/${productId}")
            .check(status.is(200)))
          .randomSwitch(
           70 -> pause(1 second, 5 seconds),
           30 -> exec(
             http("Add a product in cart")
                 .post("/cart/add")
                 .param("product", "${productId}")
                 .param("quantity", "1")
                 .check(status.is(200))
                 .check(regex(numberOfProductsRegex).find.transform(_.map(_.toInt)).exists.saveAs(numberOfProducts))))
    }
  }
}

On va d’abord vérifier que le panier de l’utilisateur courant est bien vide. La méthode transform() est nécessaire pour convertir le nombre de produits retrouvés en Int :

check(regex(numberOfProductsRegex).find.transform(_.map(_.toInt)).is(0).saveAs(numberOfProducts)))

On itère ensuite avec asLongAs() pour que le panier contienne au moins 3 produits :

asLongAs(_.get[Int](numberOfProducts, Int.MaxValue) < 3)

Et on effectue un randomSwitch() pour simuler le fait qu’un utilisateur ajoute un produit sur trois en moyenne lorsqu’il le consulte.

Maintenant, le but est d’effectuer une simulation combinant ces 3 scénarios afin de réaliser un stress test. Pour cela, on reprend notre fichier ‘BeesShopSimulation.scala‘ où l’on va ajouter nos deux nouveaux scénarios à l’initialisation, et les configurer de la façon suivante :

  • un client consulte 5 produits aléatoires → ~65 utilisateurs/s pendant 2 minutes;
  • un client ajoute un produit sur trois à son panier lorsqu’il le consulte → rien pendant 1 minute, puis ~75 utilisateurs/s pendant 40 secondes;
  • un client recherche un produit et écrit un commentaire dessus → rien pendant 1"30, puis 500 utilisateurs d’un seul coup.
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class BeesShopSimulation extends Simulation {

  val httpConf = httpConfig.baseURL("http://localhost:8085/bees-shop")

  setUp(
    ConsultProductsScenario.scn
      .inject(ramp(8000 users) over (2 minutes))
      .protocolConfig(httpConf),
    AddProductsInCartScenario.scn
      .inject(nothingFor(1 minutes), ramp(3000 users) over (40 seconds))
      .protocolConfig(httpConf),
    SearchAndCommentProductsScenario.scn
      .inject(nothingFor(100 seconds), atOnce(500 users))
      .protocolConfig(httpConf)
  )
}

De même que précédemment, on exécute la simulation à l’aide de la CLI, et l’ensemble des résultats est disponible ici.

Voici le graphe montrant le nombre de requêtes par seconde, avec :

  •  en orange: le nombre de session utilisateur active à un instant donné.

Capture+d’écran+2013-07-01+à+17.11.45

Comme vous pouvez le constater, nous avons dépassé les 1 000 utilisateurs simultanés lors du pic de charge, ce qui a eu pour conséquence de faire échouer 50% des requêtes à ce moment là.

Conclusion

Vous l’aurez compris Gatling est une vraie alternative aux autres outils de test de performances, sa simplicité d’usage, son efficacité, et sa fiabilité, vous permettront en quelques minutes de réaliser un test de montée en charge, un test de stress, ou bien encore un test aux limites. C’est un outil qui est libre, en constante évolution, et cela serait dommage de s’en priver dans nos projets.

L’ensemble du code source des scénarios et des simulations, ainsi que de l’application cible, est disponible ici sur mon compte GitHub. C’est aussi pour vous, l’occasion de voir un exemple d’intégration de Gatling avec Maven, et aussi de Java avec Scala dans un même projet. N’hésitez pas à forker le projet, à coder vos propres tests, et à nous faire partager vos découvertes ou difficultés rencontrées.

Pour conclure, on peut dire avec conviction que Gatling aura eu raison une fois de plus du Bees Shop wink

5 réflexions au sujet de « Gatling, ou comment écrouler un serveur – alternative à JMeter »

  1. Publié par Laurent Bristiel, Il y a 4 années

    Merci pour cet excellent article très détaillé !

    J’ajouterai juste un point, c’est que Gatling propose aussi un recorder qui apport 2 choses très interessantes :

    1) il permet d’initialiser son scénario en le jouant « à la main » dans son browser avec le recorder en proxy. On a donc la liste de GET, POST etc. avec les bonnes URL. Il faut le revoir bien sur (ajouter des feeder par exemple), mais c’est un bon point de départ

    2) il enregistre automatiquement les pauses entre les requetes. Que ce soit des pauses « techniques » (temps que met le browser à parser le javascript avant de lancer une autre requete lancée par le javascript en question) ou des pauses « humaines » (le client ne clique pas immédiatement sur un lien, il attend). Ceci rend les scénarios beaucoup plus réalistes.

    Laurent

  2. Publié par chris, Il y a 4 années

    Super article !

    Quand je pense que je me suis fais ch**r récemment avec jMeter pour écrire mon propre AbstractJavaSamplerClient afin de faire des requêtes HTTP JSON paramétrées …

    Dommage que je n’ai pas le temps de revenir la dessus pour essayer Gatling :(

    Juste quelques questions du coup :
    1. tu mets tes tests dans src/test/scala et ensuite tu utilises gatling-maven-plugin pour lancer ces tests : comment Gatling fait pour choisir s’il y a plusieurs Simulation disponibles ? Par ex, il génère un rapport par simulation ?

    2. comment tu fais pour injecter des paramètres depuis la CLI ? Par ex, si tu veux tester staging.monapp.net et aussi prod.monapp.net tu es obligé de faire 2 classes ?

    3. comment ut fais pour injecter des paramètres depuis le pom.xml ? Par ex depuis un profile maven qui contiendrait l’URL ?

    Merci encore pour l’article

  3. Publié par Clément Lardeur, Il y a 4 années

    @Laurent
    Merci pour ton commentaire et d’avoir mentionné le recorder.

    @Chris
    1. Avec le plugin Maven, tu peux préciser la simulation à exécuter en paramètre:
    $ mvn gatling:execute -Dgatling.simulationClass=BeesShopSimulation

    2-3. Non pas besoin de faire 2 simulations différentes. Plusieurs solutions existent, la plus simple à mon avis est de créer un fichier de properties par env (dev, integ, prod) et d’indiquer lequel utiliser via une propriété système :
    $ mvn gatling:execute -Dgatling.simulationClass=BeesShopSimulation -Denv=dev

    Si tu utilises la CLI alors il faut enrichir le script ‘gatling.sh’ afin d’ajouter ta variable d’environnement aux JAVA_OPTS :
    $ ./gatling.sh -e dev

  4. Publié par Philippe Bossu, Il y a 4 années

    Article intéressant et détaillé, bravo.
    Mon opinion sur les critiques à l’égard de JMeter ainsi que Gatling sont exposés ici:
    http://bit.ly/18LjAX8
    Pour Chris, pas besoin de coder un AbstractJavaSamplerClient pour faire du json en jmeter, un simple Http Request avec un raw post body variabilisé et c’est bon.

  5. Publié par sabah, Il y a 2 années

    Merciiiiiiiiiiiiiiii bcp pour cette explication Bravoo

  6. Publié par Kamal, Il y a 1 mois

    Bonjour, le lien vers le site The Bees Shop ne fonctionne plus. Pourriez-vous le changer afin de pouvoir suivre au mieux le tuto. Merci

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *