Publié par
Il y a 7 années · 15 minutes · Java / JEE, Performance

Performance – Maîtriser son framework de test, The Grinder

La performance a été souvent considérée comme étant le parent pauvre des applications. Afin de combler ce défaut et de détecter les éventuels points de faiblesse des applications, plusieurs outils propriétaires et open source ont vu le jour sur le marché: Compuware/Qaload, LoadRunner, OpenSTA, JMeter, etc, et notamment The Grinder. L’adoption de ce dernier fut moins évidente que celle de son homologue côté Apache, du fait de l’absence de support et d’une interface GUI pour la définition, la configuration et le paramétrage des scripts ; ce manque de l’aspect « cliquodrome » a fait croire aux utilisateurs que l’outil est à mettre uniquement dans les mains d’un développeur python, et a également conduit à en dissimuler les talents.
Le but de cet article est de donner un ensemble de guidelines pour faire un meilleur usage de l’outil.

Le framework The Grinder

C’est quoi The Grinder ?

The Grinder est un framework de test de charge « Jython-based scripting » écrit en Java. Il est open source (sous licence BSD) et  hébergé chez sourceForge.

Il permet de tester:

  • Les serveurs web en mode HTTP /HTTPS.
  • Tout ce qui vit dans un serveur d’application (Web service SOAP/REST, EJB, JMS,…)
  • Les bases de données avec JDBC.
  • Les servers FTP, POP3, SMTP, LDAP.

The Grinder s’exécute sur une plateforme supportant une version Java 1.4 ou supérieur.

Il peut être utilisé à des fins telles que :

  • Test fonctionnel : contrôler le rendu de l’application par rapport au résultat attendu.
  • Test de charge : vérifier que l’application peut supporter une charge donnée.
  • Test de capacité : connaître la charge maximale supportée par l’application avant l’apparition d’erreurs
  • Test de stress : vérifier la stabilité de l’application.

Historique

The Grinder, « Le moulin », a été initialement développé pour les besoins du livre J2EE Performance Testing with BEA WebLogic Server par Paco Gómez et Peter Zadrozny.

Par la suite, Philip Aston a pris possession du code et l’a retravaillé pour en créer The Grinder 2.

C’est en Juillet 2003 que la première édition du livre fut publiée par Peter, Philippe et Ted Osborne, faisant ainsi un usage intensif de l’outil à sa deuxième version (The Grinder 2).

Peu après, Philip Aston commence à travailler sur The Grinder 3. Cette version offre de nombreuses nouvelles fonctionnalités, la plus importante est le Scripting Jython.

Environnement de développement

Architecture & Conception des Scripts

Architecture

Il est important de comprendre l’architecture du framework,  ses différents composants et son mode fonctionnement.

Afin d’illustrer ce propos, je vous propose les deux schémas ci-dessous ;  l’un qui fait apparaitre les composants de base d’une plateforme d’injection d’un point de vue conceptuel, et l’autre illustrant l’architecture et les terminologies spécifiques à The Grinder.

Design View

Architecture

The Grinder View

Architecture2
  • La console contrôle les workers (les injecteurs) par le biais des agents et se charge d’agréger les résultats de test de l’ensemble des injecteurs de tous les agents.
  • Les agents sont en charge de piloter un ou plusieurs worker (généralement une instance « agent » est créé par JVM).
  • Les workers (grinder.processes) permettent d’injecter la charge au système à tester en démarrant le nombre de thread spécifié par la clé « grinder.threads » (les utilisateurs virtuels) et remontent les statistiques à la console. Chaque thread ainsi créé, exécute le script de test « grinder.runs » fois.

Conception des scripts et des scénarios

Le diagramme ci-dessous permet d’illustrer la séquence d’exécution d’un script grinder (gérée par le framework) couplée avec celle d’un scénario composé de deux tâches (gérée par le développeur) :

DiargrammeSéquence

Généralement les scénarios fonctionnels à dérouler se différencient  en bout de chaîne, ou plus simplement dit, partagent un certain  nombre de transactions (Tâches) ; Ce qui fait apparaître le besoin de décomposer vos scénarios en un ensemble de tâches réutilisables.

Le lien entre deux tâches successives (récupérer l’output d’une tâche pour le faire passer en input de la tâche suivante) est réalisé par la requête suivante:

HTTPPluginControl.getHTTPUtilities().getLastResponse()

Un scénario est donc défini par la succession d’un ensemble de tâches. Il encapsule la logique d’exécution de ses tâches (Scenario.run()).

Un « ScenarioAScript » permet la création et l’initialisation d’un « Scenario » composé de deux tâches « Task1 » et « Task2 ». Il est également responsable de la gestion de la rampe pour son scénario se traduisant généralement par une attente d’une durée égale au « pacingTime » (le temps d’attente entre le lancement de la première exécution de deux Threads successifs).

Un « Tir » constitue le script d’entrée pour The Grinder, il joue le rôle d’un orchestrateur et permet de répartir les threads sur l’ensemble des scénarios (affectation Thread / ScenarioXScript) selon la configuration souhaitée.

L’exemple ci-dessous permet de répartir les threads sur 3 scénarios avec les proportions suivantes : le  ScenarioAScript 50%, le ScenarioBScript 25 % et le ScenarioCScript 25 %

from net.grinder.script.Grinder import grinder

scripts = ["ScenarioAScript", "ScenarioBScript", "ScenarioCScript"]

# Ensure modules are initialised in the process thread.
for script in scripts: exec("import %s" % script)

def createTestRunner(script):
    exec("x = %s.TestRunner()" % script)
    return x

class TestRunner:
    def __init__(self):
        tid = grinder.threadNumber
        if tid % 4 == 2:
            self.testRunner = createTestRunner(scripts[1])
        elif tid % 4 == 3:
            self.testRunner = createTestRunner(scripts[2])
        else:
            self.testRunner = createTestRunner(scripts[0])

    # This method is called for every run.
    def __call__(self):
        self.testRunner()

Ce qu’il faut savoir

Réinitialisation des cookies au lancement de chaque run

Il faut savoir qu’à chaque run, The Grinder effectue un « cookie-reset » ; ce qui pourrait être très embêtant dans le cas où votre démarche de test s’inscrit dans la logique suivante:

  • S’authentifier (création de la session, initialisation des cookies)
  • Boucler sur le scénario fonctionnel n fois
  • Se déconnecter

Pour pallier ce problème, la solution la plus simple consiste à récupérer l’ensemble des cookies initialisés au démarrage de la session lors du premier run et les injecter par la suite dans les runs qui suivent comme suit :

# récupérer l’objet HTTPClientContext pour le thread en cours au premier run.
threadContext = HTTPPluginControl.getThreadHTTPClientContext()

#récupération de l’ensemble des cookies au premier run
cookies=CookieModule.listAllCookies(threadContext)

      # récupérer l’objet HTTPClientContext pour le thread en cours.
      threadContext = HTTPPluginControl.getThreadHTTPClientContext()

      # Injection des cookies dans le header http du thread en cours
      for cookie in cookies:
          CookieModule.addCookie(cookie, threadContext)

Une deuxième solution est envisageable ; Il s’agit de désactiver la gestion automatique des cookies :

HTTPPluginControl.getConnectionDefaults().useCookies = 0

et de créer son propre « handler » de cookie :

# un « implements » à la python de l’interface «CookiePolicyHandler»
class MyCookiePolicyHandler(CookiePolicyHandler):

    # implement your own cookie acceptance policy.
    def acceptCookie(self, cookie, request, response):
        # traitement souhaité
        return 1
    # control the sending of cookies according to the matching rules
    # for the path, domain, protocol, etc
    def sendCookie(self, cookie, request):
        # traitement souhaité
        return 1

Par la suite, quelque part dans vos scripts, vous devrez faire un « set » du custom-handler :

CookieModule.setCookiePolicyHandler(MyCookiePolicyHandler())

Http/GET non implicite des ressources statiques

Les requêtes http exécutées par le framework ne permettent pas de récupérer systématiquement les ressources statiques (cacheables par défaut) liées à la page demandée, comme les fichiers de type *.css, *.jpeg, *.gif, *.jar,… Il faut donc penser à les récupérer explicitement (par un http/GET du fichier en question) dans le cas où vous jugerez que cela pourrait avoir un impact sur les résultats de test.

Gestion des « runs »

A un certain moment, lors du développent de vos scripts,  vous auriez été tentés de gérer le nombre d’exécutions (grinder.runs) par vous-mêmes afin de contourner certains problèmes, notamment celui du cookie-reset précédemment évoqué. Je comprendrais que vous eussiez été plus à l’aise avec un « loop » et des « if » pour implémenter toute la logique d’exécution souhaitée, mais vous auriez sans doute constaté que les temps de réponse augmentaient d’une manière aberrante et proportionnelle au nombre de VUs (grinder.threads) ; ce qui est certainement lié à la contention des ressources et des threads dans vos « loop ». Je voulais donc en venir à la règle suivante: « Déléguer au framework ce qu’il a à faire reste un principe fondamental de son utilisation à ne pas négliger ».

Externalisation et centralisation de la configuration au niveau de grinder.properties

Mettre toute la configuration de vos scripts dans le fichier grinder.properties vous permettra de regrouper l’ensemble des paramètres d’une manière centralisée: «  un seul fichier à maintenir ». Vous disposez nativement de la méthode «  grinder.getProperties ()«clé» pour récupérer les valeurs des différentes clés (pas besoin d’une classe utilitaire avec un java.util.Properties.load()).

Gestion des exceptions

N’hésitez pas à renforcer vos tests par des blocs de try / catch afin de passer le test précédemment exécuté ou encore celui qui est en cours d’exécution en « fail ».

Il faudra également distinguer les erreurs techniques liées au traitement de la requête par le serveur (serveur injoignable, http code = ! 200,…) des erreurs fonctionnelles liées à la logique du déroulement du scénario (jeu de données invalide, des inputs introuvables, rendu de la page incohérent,…).

Ci-dessous une manière de faire:

import sys, traceback

request = Test(1, "Basic request").wrap(HTTPRequest(url = "http://localhost:7001"))
     try:
        result = request.GET("index.html")

     except:
        # mark the test as a failure
        grinder.statistics.forLastTest.success = 0

        # tracer l’erreur d’origine:
        grinder.getLogger().error("Unexpected error: %s: %s: %s" % (sys.exc_info()[0], sys.exc_info()[1], traceback.print_exc()))

        # throw new ExceptionTechnique
        raise ExceptionTechnique(« message d’erreur »)

     # Si le traitement dans le bloc try est exécuté
     # correctement, on vérifie que le status du dernier test est
     # OK, et que le code retour http == 200
     else:
        # récupérer l’objet Statistique pour le dernier test
        statisticsForTest = grinder.statistics.forLastTest

        # Si le test s’est terminé avec succès  et
        # http reponse code == 200
        if statisticsForTest.success and
           statisticsForTest.getLong("httpplugin.responseStatus")
           == 200:
            try:
               Dérouler votre checkList sur le rendu attendu
            except:

               # mark the test as a failure
               grinder.statistics.forLastTest.success = 0

               # tracer l’erreur d’origine:
               grinder.getLogger().error("Unexpected error: %s: %s: %s" % (sys.exc_info()[0], sys.exc_info()[1], traceback.print_exc()))

               # throw new ExceptionFocntionnelle :
               raise ExceptionFonctionnelle(« message d’erreur »)

        # sinon : erreur lors du traitement de la requête
        else:
            # mark the test as a failure
            grinder.statistics.forLastTest.success = 0

            # throw new ExceptionTechnique
            raise ExceptionTechnique(« message d’erreur »)

Parsing XML et XPath:

Le parsing xml de la réponse constitue une étape quasi-évidente pour dérouler votre check-list sur les éléments attendus et également pour récupérer les valeurs de certains d’entre eux ; ainsi, vous pourrez savoir s’il est possible de passer à l’étape suivante de votre scénario.

En terme de technologie adoptée, je vous laisse faire votre choix entre l’utilisation des APIs standards fournies avec le JDK (javax.xml.parsers, org.xml.sax, javax.xml.xpath) et l’API vtd-xml qui est plus puissante et moins consommatrice (basée sur la technique Virtual Token Descriptor (VTD))

Accès concurrents aux ressources « statiques »

Si vous manipulez des variables statiques dans vos scripts python, vous êtes sûrement face à un problème d’accès concurrentiel du moment que vous avez plus d’un injecteur sur une même JVM (grinder.processes>1).

On reconnaît ce cas de figure par le positionnement du mot clé « global » sur une variable précédemment déclarée dans le script, dans le but de pourvoir modifier sa valeur (accès en écriture) à l’intérieur d’une méthode :

maVariable = 0
   def incrementMaVariable() :
      global maVariable
      maVariable += 1

Pour remédier à ce problème, la solution la plus évidente consiste clairement à éviter d’employer ce genre de pratique qui n’est pas en phase avec l’architecture distribuée de The Grinder.
Néanmoins si vous y tenez, il est possible d’utiliser la classe Lock du module threading.py:

import threading
# récupérer un 	Object Lock pour la gestion d'accès concurrent à la ressource " maVariable "
lock = threading.Lock()

maVariable = 0
    def incrementMaVariable() :
       try:
           # will block if lock is already held
           lock.acquire()

           # lecture de la variable
           global maVariable

           # écriture de la variable
           maVariable += 1

       finally:
           # release lock
           lock.release()

Nommage des transactions et unicité des identifiants de test

Pensez à nommer vos transactions d’une manière homogène et ordonnée ; cela vous facilitera la lecture des résultats de test par la suite.
Le modèle suivant pourrait apporter un certain confort de lisibilité et de clarté : SC-<Référence du scénario>-STEP-<Numéro de la transaction>)<Libellé de la transaction>

Prenons par exemple le scénario « consultation du profil », composé de trois transactions à savoir : login, consulter profil et logout.
Cela aurait donné quelque chose de ce genre lors de la définition des tests:

request1 = Test(1, "SC-CONSULTATION-PROFIL-STEP-1)Login")
request2 = Test(2, "SC-CONSULTATION-PROFIL-STEP-2)Consultation du profil")
request3 = Test(3, "SC-CONSULTATION-PROFIL-STEP-3)Logout")

Les identifiants de vos tests doivent être uniques dans un scope «grinder-project» ; cela vous permettra d’une part d’établir rapidement le lien entre le test id et son libellé, et d’autre part d’éviter toute confusion de transaction (un même identifiant pour deux transactions différentes, bien qu’il soit toujours possible de les distinguer par le libellé).

N’optez pas pour des variables statiques (avec un système d’incrémentation), vous risquez de vous replonger dans le problème d’accès concurrents à partir du moment où vous êtes dans le cas d’une configuration multi-process par JVM; à la limite gardez-les en dur.

Intégration dans le Build PipeLine

Il existe un plugin Hudson : http://wiki.hudson-ci.org/display/HUDSON/Grinder+Plugin:

Ce plugin permet d’exécuter les scripts « grinder » pendant le build et d’inclure les résultats de tests obtenus dans le rapport hudson.

Génération des rapports graphiques

Il existe principalement deux outils :

  • Grinder Analyzer : Permet de parser les logs grinder et de générer des rapports graphiques (au format html). L’utilisation de cet outil reste très simple et suffisante  pour un aperçu visuel des résultats lorsqu’il s’agit d’un environnement de test mono-agent et mono-worker. Cependant, il trouve rapidement ses limites face à des données  issues de plusieurs « agents » et « workers », ou également lorsqu’il s’agit de générer des graphiques de comparaison entre deux tests.
  • ground-report : Produit un ensemble de rapports graphiques (rapport de synthèse, rapport individuel par test, rapport de comparaison entre plusieurs tests,…) au format pdf et à partir d’une base de données PostgreSQL. Il est également possible de produire des rapports au format rtf, xml  ou xhtml.
    • Démo: http://ground.sourceforge.net/samples.html
    • Il se distingue par :
      • La qualité de ses rapports.
      • Configuration rapide.
      • La sauvegarde des résultats de test en base de données garantissant ainsi d’avoir l’historique de l’ensemble tests.
      • Son « ToolKit » pour les opérations d’injection des données « grinder » en base.

Les limites

  • Ne produit pas nativement des graphiques.
  • Ne permet pas de simuler des sessions à partir d’adresses IP différentes, ce qui constitue  généralement un point très important lorsque le target system  est « load-balancé » (souvent le cas en production).
  • Manque du support.

Conclusion

Dans la mesure où les tests de performance s’imposent de plus en plus, comme c’est le cas actuellement pour les tests unitaires, les tests d’intégration et les tests fonctionnels qui ne cessent de se greffer dans nos projets, il me semble envisageable de compléter la stack de test en faisant d’eux une partie intégrante des projets; ce qui nous permettra de les faire constamment marier aux évolutions que subissent les applications.

Aujourd’hui on parle d’un test unitaire cassé, on pourrait bien parler d’un script de test de charge cassé.

Pour peu que l’on ait conçu et développé ses scripts proprement (sans pour autant frôler la limite de l’over-design), on gagne énormément en maintenabilité: modifier le comportement d’un script, c’est comme si l’on s’apprêtait à modifier une  méthode d’un service.

Le fait que The Grinder s’appuie sur du Jython (implémentation de Python en Java),  on garde toujours un même environnement d’exécution; que ce soit du .java  vers du .class ou du .py vers $py.class jusqu’au runtime, c’est toujours la JVM qui bosse. En plus, vous continuez à profiter de toutes les API Java; ce qui vous offre le moyen de rester dans vos habitudes et de ne pas forcément glisser dans une démarche full-python.

Une réflexion au sujet de « Performance – Maîtriser son framework de test, The Grinder »

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

    Bonjour,
    J’ai lu avec attention cet article… et puis je me suis demandé. Lorsqu’on réalise un audit de performance, doit-on consacrer une énergie importante au développement des scripts ou doit-on analyser prioritairement le fonctionnement de l’application. Je pense sincèrement que vous avez fait la démonstration que cet outil est à mettre uniquement dans les mains d’un développeur python. Il y a tellement d’autres outils beaucoup plus simple à utiliser qui permettent d’aboutir rapidement à des scripts opérationnels et qui permettent surtout de se focaliser sur l’objectif.

  2. Publié par AH, Il y a 1 année

    Bonjour,

    je m’intéresse à cette outil, mais il est vrai qu’il n y a pas grand monde qui l’utilise. auriez vous des tutos?

    Merci de ne pas publier ce commentaire

    par avance merci

Laisser un commentaire

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