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