Utilisation de gatling

Je ne présente pas gatling. Je pense que tout le monde a dû entendre parler de près ou de loin de cet outil permettant de tester la montée en charge d’une application web en implémentant des scénarii, le nombre d’utilisateurs à la seconde, etc.

Ce qui va nous intéresser ici est l’implémentation d’une solution simple.

La première chose à faire est de créer un répertoire gatling (pour l’exemple, je l’ai fait à la racine de l’arborescence d’un projet Symfony2). Il comportera quatre répertoires: properties, resources results et scala.

  • properties va comporter un fichier de propriétés selon les différents environnements sur lesquels vous souhaiter tester votre site: local, preprod, prod
  • resources est ici mis à titre indicatif. Il comportera deux répertoires, conf et data. Je ne me sers de conf que lorsque je créé un container docker et que je souhaite pouvoir surcharger la conf gatling à l’intérieur du container. Data comportera des fichiers csv, par exemple, pour nourrir vos scénarii.
  • results comportera les fichiers html de résultats des tests de montée en charge.
  • scala est le répertoire le plus intéressant car il comporte les fichiers de scénarii.

Le fichier properties/test-parameters.properties

#####################
# GLOBAL PARAMETERS #
#####################

#platform=preprod
platform=prod

platform.preprod.host=www.preprod.www.jpsymfony.com
platform.prod.host=www.jpsymfony.com


###########################################
# PARAMETERS FOR TEST: home #
###########################################

home.preprod.users=100
home.preprod.rampTimeSeconds=25
home.preprod.minResponseTime=10
home.preprod.meanResponseTime=4000
home.preprod.maxResponseTime=8000
home.preprod.allowedFailurePercent=0

home.prod.users=10
home.prod.rampTimeSeconds=10
home.prod.minResponseTime=10
home.prod.meanResponseTime=5000
home.prod.maxResponseTime=30000
home.prod.allowedFailurePercent=0

Le fichier scala/Config.scala

Il va nous permettre d’interroger le fichier test-parameters.properties pour en récupérer les informations renseignées:

package jpsymfony

import java.io.FileInputStream
import java.io.IOException
import java.io.InputStream
import java.util.Properties

class Config(val testName: String) {

    private var properties: Properties= _

    properties = new java.util.Properties()
    var input: InputStream = null
    try {
        input = new FileInputStream("properties/test-parameters.properties")
        properties.load(input)
    } catch {
            case ex: IOException => ex.printStackTrace()
    } finally {
            if (input != null) {
                try {
                    input.close()
                } catch {
                    case e: IOException => e.printStackTrace()
                }
            }
    }

    val platform = properties.getProperty("platform")
    val host = properties.getProperty("platform." + platform + ".host")

    def get(name: String): String = {
        val propertyName = testName + "." + platform + "." + name
        val propertyValue = properties.getProperty(propertyName)
        if (propertyValue == null) {
            throw new Error("Property not defined: " + propertyName)
        }
        return propertyValue
    }

    def getInt(name: String): Int = {
        return get(name).toInt
    }

    def nbUsers(): Int = { return getInt("users") }
    def rampTimeSeconds(): Int = { return getInt("rampTimeSeconds") }
    def minResponseTime(): Int = { return getInt("minResponseTime") }
    def meanResponseTime(): Int = { return getInt("meanResponseTime") }
    def maxResponseTime(): Int = { return getInt("maxResponseTime") }
    def allowedFailurePercent(): Int = { return getInt("allowedFailurePercent") }

    def usersPerSecond(): Double = {
        return ((1.0*nbUsers.intValue())/rampTimeSeconds.intValue())
    }

    def testLabel(): String = {
        return testName + " - " + usersPerSecond + " users per second on " + platform
    }

}

Rien de compliqué:

  1. J’ouvre le fichier test-parameters.properties et je lance une erreur si je ne le trouve pas ou qu’il n’est pas lisible:
try {
        input = new FileInputStream("properties/test-parameters.properties")
        properties.load(input)
    } catch {
            case ex: IOException => ex.printStackTrace()
    } finally {
            if (input != null) {
                try {
                    input.close()
                } catch {
                    case e: IOException => e.printStackTrace()
                }
            }
    }

2.   Je récupère la plateforme (preprod ou prod) et le host correspondant:

val platform = properties.getProperty("platform")
val host = properties.getProperty("platform." + platform + ".host")

3.    Je définis une fonction, get, qui va rechercher la propertyName (exemple home.prod.minResponseTime), puis sa value correspondante et la retourner

    def get(name: String): String = {
        val propertyName = testName + "." + platform + "." + name
        val propertyValue = properties.getProperty(propertyName)
        if (propertyValue == null) {
            throw new Error("Property not defined: " + propertyName)
        }
        return propertyValue
    }

4.    Je définis une seconde fonction, getInt, qui va transformer en integer la value retournée par la fonction get()

def getInt(name: String): Int = {
        return get(name).toInt
    }

5.    Enfin, je définis une série de fonctions qui vont appeler getInt, en fonction de la propriété recherchée

    def nbUsers(): Int = { return getInt("users") }
    def rampTimeSeconds(): Int = { return getInt("rampTimeSeconds") }
    def minResponseTime(): Int = { return getInt("minResponseTime") }
    def meanResponseTime(): Int = { return getInt("meanResponseTime") }
    def maxResponseTime(): Int = { return getInt("maxResponseTime") }
    def allowedFailurePercent(): Int = { return getInt("allowedFailurePercent") }

6.     Enfin, je définis deux fonctions (la seconde appelant la première), usersPerSecond et testLabel afin de retourner, à chaque test, un texte en rapport avec le nombre d’utilisateurs par seconde

    def usersPerSecond(): Double = {
        return ((1.0*nbUsers.intValue())/rampTimeSeconds.intValue())
    }

    def testLabel(): String = {
        return testName + " - " + usersPerSecond + " users per second on " + platform
    }

Le fichier scala/performance/home.scala

Il est temps d’écrire notre premier scénario simple qui va interroger une url et nous en sortir des statistiques:

package jpsymfony.performance

import java.util.Calendar
import java.util.concurrent.atomic.AtomicInteger

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

class Home extends Simulation {

    val testName = "home"
    val config = new jpsymfony.Config(testName)

    val calendar = Calendar.getInstance()
    val startDate: Long = calendar.getTimeInMillis() / 1000
    val requestNumber = new AtomicInteger()

    val httpConf = http
    .baseURL("http://" + config.host)

    val scn = scenario(config.testLabel)
    .exec(http("arrivée sur le site").get("/?_timestamp=(startDate + requestNumber.incrementAndGet()).toString)"))

    setUp(scn.inject(rampUsers(config.nbUsers) over (config.rampTimeSeconds seconds))
    .protocols(httpConf))
    .assertions(global.responseTime.min.greaterThan(config.minResponseTime))
    .assertions(global.responseTime.mean.lessThan(config.meanResponseTime))
    .assertions(global.responseTime.max.lessThan(config.maxResponseTime))
    .assertions(global.failedRequests.percent.is(config.allowedFailurePercent))
}
  • Je définis le testName (utilisé dans Config)
  • Je définis la config avec un new jpsymfony.config(testName)
  • Je fais appel à la librairie Calendar pour créer un timestamp qui changera à chaque appel (pour que varnish ne me retourne pas la réponse en cache dès le second appel)
  • le host est défini (ici platform.prod.host, soit www.jpsymfony.com
  • Je crée le scénario en faisant appel à config.testLabel pour que le texte affiché soit conforme avec le nombre d’utilisateurs qui rentrent par seconde
  • Je demande à injecter les utilisateurs sur un nombre de secondes définis dans le fichier test-parameters.properties (plutôt que tous d’un coup) et je vérifie un certain nombre d’assertions (qui, pour récupérer les valeurs, font toutes appel aux fonctions définies dans le fichier Config.scala)

Juste avant de lancer le test, je tiens à vous montrer ce que vous pouvez faire si vous placez des csv dans le répertoire data, avec par exemple l’entête id (donc une seule colonne ayant une entête id, par exemple:

id
1
2
3
    object Request {

        val csvFeeder = csv("moncsv.csv");

        val request =
            feed(csvFeeder)
            .exec(
                http("get my resource")
                .get("/monUrl/${id}?_timestamp=(startDate + requestNumber.incrementAndGet()).toString)"))

    }

Et pour le test, il faut changer la fin:

    val scn = scenario(config.testLabel)
    .exec(Request.request)

C’est parti pour lancer le test!

Je présume que vous avez installé gatling en suivant la doc du site. Nous allons donc lancer la commande en nous plaçant dans le répertoire gatling:

sudo /opt/gatling/bin/gatling.sh -rf results -sf scala/

-rf indique où les résultats doivent se placer, et -sf où les scénarii se trouvent:

GATLING_HOME is set to /opt/gatling
jpsymfony.performance.Home is the only simulation, executing it.
Select simulation id (default is 'home'). Accepted characters are a-z, A-Z, 0-9, - and _

Select run description (optional)

Simulation jpsymfony.performance.Home started...

================================================================================
2016-09-25 16:53:01                                           5s elapsed
---- home - 1.0 users per second on prod ---------------------------------------
[-------------------------------------                                     ]  0%
          waiting: 5      / active: 5      / done:0     
---- Requests ------------------------------------------------------------------
> Global                                                   (OK=2      KO=0     )
> arrivée sur le site                                      (OK=2      KO=0     )
================================================================================


================================================================================
2016-09-25 16:53:06                                          10s elapsed
---- home - 1.0 users per second on prod ---------------------------------------
[#######-------------------------------------------------------------------] 10%
          waiting: 0      / active: 9      / done:1     
---- Requests ------------------------------------------------------------------
> Global                                                   (OK=6      KO=0     )
> arrivée sur le site                                      (OK=5      KO=0     )
> arrivée sur le site Redirect 1                           (OK=1      KO=0     )
================================================================================

...

Simulation jpsymfony.performance.Home completed in 25 seconds
Parsing log file(s)...
Parsing log file(s) done
Generating reports...

================================================================================
---- Global Information --------------------------------------------------------
> request count                                         20 (OK=20     KO=0     )
> min response time                                   2101 (OK=2101   KO=-     )
> max response time                                   9969 (OK=9969   KO=-     )
> mean response time                                  6490 (OK=6490   KO=-     )
> std deviation                                       2558 (OK=2558   KO=-     )
> response time 50th percentile                       6926 (OK=6926   KO=-     )
> response time 75th percentile                       8873 (OK=8873   KO=-     )
> response time 95th percentile                       9178 (OK=9178   KO=-     )
> response time 99th percentile                       9810 (OK=9810   KO=-     )
> mean requests/sec                                  0.769 (OK=0.769  KO=-     )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms                                             0 (  0%)
> 800 ms < t < 1200 ms                                   0 (  0%)
> t > 1200 ms                                           20 (100%)
> failed                                                 0 (  0%)
================================================================================

Reports generated in 0s.
Please open the following file:  /Users/johnsaulnier/Sites/testGatling/gatling/results/home-1474815546809/index.html
Global: min of response time is greater than 10 : true
Global: mean of response time is less than 5000 : false
Global: max of response time is less than 30000 : true
Global: percentage of failed requests is 0 : true

Il n’y a plus qu’à se rendre sur la page html pour voir les résultats à l’endroit indiqué:

gatling

gatling2

Avant de nous quitter, pour les habitués à docker, voici ma configuration:

gatling:
    container_name: gatling
    image: webdizz/gatling
    volumes:
      - /var/www/testGatling/gatling/properties:/opt/gatling/properties
      - /var/www/testGatling/gatling/scripts:/scripts
      - /var/www/testGatling/gatling/scala:/opt/gatling/user-files/simulations
      - /var/www/testGatling/gatling/resources/data:/opt/gatling/user-files/data
      - /var/www/testGatling/gatling/resources/bodies:/opt/gatling/user-files/bodies
      - /var/www/testGatling/gatling/resources/conf/gatling.conf:/opt/gatling/conf/gatling.conf
      - /var/www/testGatling/gatling/resources/conf/application.conf:/opt/gatling/conf/application.conf
      - /var/www/testGatling/gatling/results:/opt/gatling/results

Ici, deux répertoires ont été rajoutés: scripts et bodies. bodies est utilisé lorsqu’on détermine des bodies json, par exemple.

{
    "my_entity_id":"${id}",
    "url_source":"http://www.myurl",
    "token":"${token}"
}

Ce body peut être utilisé lors d’un post (il faut bien sûr avoir un feeder pour que ${token} et {id} soient renseignés:

http("get token " + token)
    .post("http://myurl." + config.host)
    .body(ELFileBody("myjson.json")).asJSON

 

Le répertoire scripts contient un script non_reg.sh qui me permet de lancer tous les tests d’un container:

rm -rf /opt/gatling/results/* && cd /

tests=(Home)

for test in ${tests[@]}
do
    echo '####### LANCEMENT DU TEST : '${test}' #######'
    /opt/gatling/bin/gatling.sh -sf /opt/gatling/user-files/simulations -s "jpsymfony.performance."${test} -rf /opt/gatling/results/
    echo '####### FIN DU TEST : '${test}' #######'
done

Ainsi, je n’ai plus qu’à créer un alias pour exécuter tous les tests:
alias gatling_non_reg=’docker exec -t jpsymfony-gatling /scripts/non_reg.sh’

Je peux aussi créer un alias pour que l’on me propose de choisir le test à exécuter:
alias gatling_test=’docker exec -it jpsymfony-gatling /opt/gatling/bin/gatling.sh -rf /opt/gatling/results/’

Je peux alors lancer la commande gatling_test -s jpsymfony.performance.Home  pour lancer un test en particulier ou gatling_test  pour que l’on me demande quel test exécuter.

Rédigé par

Laisser un commentaire

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

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.