Lasttests mit Grinder 3.4

by Stefan 10. June 2010 10:26

The Grinder ist ein gutes Werkzeug, um automatisch Lasttests gegen eine beliebige Anwendung zu fahren. Sehr beliebt ist dabei das Testen von Web-Anwendungen, wozu nämlich das bei Grinder mitgelieferte HTTP-Plugin bestens geeignet ist.

Grundlegendes

Um den Grinder zum laufen zu bringen, sind ein paar Dinge zu beachten, bevor es richtig losgehen kann.

Java

Grinder ist eine Java-Anwendung. Also erst einmal Java installieren, falls noch nicht vorhanden. Eine aktuelle Version ist erforderlich.

Jython-Skripte

Grinder benutzt Jython, eine implementierung der Skriptsprache Python in Java, als Skript-Sprache. Das heißt, die Lasttests, die ausgeführt werden sollen, werden in Python geschrieben (keine Angst, ist nicht schwer). Eine installation von Jython ist nicht unbedingt notwendig, da Grinder bereits den Kern von Jython, den er selbst zum Laufen braucht, mitbringt. Wenn man jedoch weitere Python-Module (z.B. re für Regular Expressions) benutzen will, braucht man eine komplette Jython-Installation und muss Grinder dazu bringen, diese zu benutzen.

Organisation

Grinder ist dazu ausgelegt, realistische Szenarien zu simulieren. So kann zum Beispiel das zu testende System von verschieden Seiten gleichzeitig mit Anfragen belastet werden. Fangen wir am besten so an: Es gibt die Grinder-Konsole, von der aus mehrere Agents bedient werden können. Jeder Agent ist normalerweise auf einem eigenen Rechner. Jeder Agent startet eine konfigurierbare anzahl an Workern. Jeder worker schreibt ein eigenes Logfile. Jeder Worker startet wiederum eine gewisse anzahl Threads. Jeder Thread lädt nun das Python-Skript in dem der Test beschrieben ist und initialisiert es. Erst dann kommt der eigentliche Spaß: Jeder Thread fährt eine, wiederum konfigurierbare, anzahl an Runs. Was in einem Run passiert, ist dann Sache des Skriptes.

Das ging jetzt vielleicht ein bisschen schnell. Ein Beispiel, dass alle Ebenen ausnutzt wäre: 2 Agents mit je 4 Workern, zu je 10 Threads mit jeweils 100 Runs. Bei diesem Szenario sind am Ende die Daten von 2*4*10*100 = 8000 Testläufen auf 8 Logfiles und 2 Rechner verteilt. Zum Glück gibt es die Grinder-Konsole, die in solch einem Szenario prima geeignet ist, da sie die Daten sammelt und einfache statistische Auswertungen macht.

In meinem kommenden Beispiel ist es jedoch viel einfacher: 1 Agent mit 1 Worker zu 2 Threads mit jeweils 5 Runs. Dies ist dahingehend gut, da ich einerseits den einen Agent später auch, ohne die Konsole zu benutzen, automatisch per Skript bedienen kann. Andererseits entsteht nur 1 Logfile, welches ich mit einem eigenen Tool auswerten kann, um detailliertere Statistiken zu erstellen, als die Grinder-Konsole es erlaubt.

Hilfs-Skripte

Obwohl die Grinder-Konsole mehrere Agents bedienen kann, muss man diese jedoch vorher selbst starten. Erst dann verbinden sie sich mit der Konsole und nehmen Befehle entgegen. Da zum Starten des Agents jedoch einige Umgebungsvariablen gesetzt werden müssen, bietet es sich an, folgende Skripte in einem Ordner abzulegen (Die Pfade natuerlich anpassen, je nach dem wo Grinder abgelegt ist):

setGrinderEnv.cmd

   1: set GRINDERPATH=C:\Projects\Camping.Info\_LoadTest\grinder
   2: set GRINDERPROPERTIES=%GRINDERPATH%\start\grinder.properties
   3: set CLASSPATH=%GRINDERPATH%\lib\grinder.jar

Falls man seine eigene Jython-Installation benutzen will:

   1: set GRINDERPATH=C:\Projects\Camping.Info\_LoadTest\grinder
   2: set JYTHONPATH=C:\Projects\Camping.Info\_LoadTest\jython2.5.1
   3: set GRINDERPROPERTIES=%GRINDERPATH%\start\grinder.properties
   4: set CLASSPATH=%JYTHONPATH%\jython.jar;%GRINDERPATH%\lib\grinder.jar

startAgent.cmd

   1: call setGrinderEnv.cmd
   2: java -cp %CLASSPATH% net.grinder.Grinder %GRINDERPROPERTIES%

startAgentWithPropertiesArg.cmd

   1: call setGrinderEnv.cmd
   2: java -cp %CLASSPATH% net.grinder.Grinder %1

startConsole.cmd

   1: call setGrinderEnv.cmd
   2: java -Duser.language="en" -cp %CLASSPATH% net.grinder.Console

Nun sollte es genug der Vorbereitung sein und es kann los gehen.

Starten von Grinder

Als allererstes starten wir die Konsole (startConsole.cmd sollte es tun):

GrinderConsole

Noch sind die Buttons zum Bedienen der Agents (oben links) deaktiviert. Das liegt daran, dass wir noch keinen Agent gestartet haben. Bevor wir das tun können, muss der Agent jedoch konfiguriert werden (wir erinnern uns an die verschiedenen Szenarien, die ich weiter oben vorgestellt habe). Dies funktioniert mit der Datei grinder.properties. Die datei kann auch anders heißen, wenn der Dateiname beim starten des Agents als Argument angegeben wird.

grinder.properties

   1: #Das hier angegebene Skript wird gestartet
   2: grinder.script = my_script.py
   3:  
   4: grinder.hostID = local
   5:  
   6: #Anzahl der Worker
   7: grinder.processes = 1
   8:  
   9: #Anzahl Threads pro Worker
  10: grinder.threads = 2
  11:  
  12: #Anzahl Runs pro Thread
  13: grinder.runs = 5
  14:  
  15: #Mit Konsole verbinden (bei false startet der Agent das Skript sofort, Daten werden immer gelogt.)
  16: grinder.useConsole = true
  17:  
  18: #Adresse anpassen, falls der Agent auf einer anderen Maschine startet als die Konsole
  19: grinder.consoleAddress = localhost
  20:  
  21: grinder.logDirectory = log
  22: grinder.numberOfOldLogs = 0
  23:  
  24: #Pfad zum Cachen von Python-Modulen. Bei Bedarf anpassen!
  25: grinder.jvm.arguments = -Dpython.cachedir=C:\\Projects\\Camping.Info\\_LoadTest\\grinder\\start\\tmp

Nun sollte man mit startAgent.cmd einen Agent starten können:

   1: 6/10/10 8:20:28 AM (agent): The Grinder 3.4
   2: 6/10/10 8:20:28 AM (agent): connected to console at localhost/127.0.0.1:6372
   3: 6/10/10 8:20:28 AM (agent): waiting for console signal

Jetzt könnte man den Agent schon loslaufen lassen, indem man auf den Button ganz links in der Toolbar der Konsole klickt. Aber halt! Wir haben das Teststkript my_script.py ja noch gar nicht geschrieben:

my_script.py

   1: import sys
   2: from net.grinder.script.Grinder import grinder
   3: from net.grinder.script import Test
   4: from net.grinder.plugin.http import HTTPRequest
   5:  
   6: request1 = HTTPRequest(url = "http://www.google.com")
   7: request2 = HTTPRequest(url = "http://www.bing.com")
   8:  
   9: def test1():    
  10:     request1.GET('/')
  11:     request1.GET('search?q=speak-friend')
  12:     
  13: def test2():    
  14:     request2.GET('/')
  15:     request2.GET('search?q=speak-friend')
  16:     
  17: google = Test(1, "Google").wrap(test1)
  18: bing = Test(2, "Bing").wrap(test2)
  19:  
  20: class TestRunner:
  21:     def __call__(self):
  22:         for i in range(3):
  23:             google()
  24:             bing()

Ganz am anfang werden Module eingebunden, so wie man das bei C# mit using machen würde.

Danach definieren wir 2 Request-Objekte. Diese HTTPRequest-Klasse ist ein spezielles Grinder-Plugin, welches automatisch Zeiten misst und statistische Daten aufzeichnet.

Daraufhin werden 2 Methoden definiert: test1() und test2(). Diese rufen jeweils für die entsprechende Suchmaschine erst die Startseite und dann die Suche nach “speak-friend” auf.

Nun kommt der komplizierteste Teil, nämlich das, was bei Grinder “Instrumentation” heißt. Erst bauen wir uns einen neuen Test, der die Nummer 1 und den Namen “Google” bekommt: Test(1, "Google") – danach wird dieser Test um unsere vorher definierte methode test1 gewrappt. Wenn man nun google() aufruft wird im Prinzip test1() aufgerufen, jedoch werden dabei aufgezeichnete Statistiken (in diesem Fall von unserem HTTPRequest) unter der Testnummer 1 mit der Bezeichnung “Google” aufgezeichnet, ins Logfile geschrieben und ggf. an die Konsole gesendet! Jeder Aufruf auf google() ergibt dabei einen eintrag in der Test-Statistik. Die aufgezeichneten Werte werden dabei addiert. Eine Zeile im Logfile bzw. ein gelisteter Test in der Konsole bezieht sich also auf die Summe aus einem Startseitenaufruf und einer Suche.

Eine Sache, die noch sehr wichtig ist: das HTTPRequest-Plugin unterstuetzt per default Cookies. Diese werden automatisch zurückgeschickt. Jedoch werden sie am anfang jedes Runs gelöscht. Daher bietet es sich an, in einem Run den Test mehrmals auszuführen. Dies wird auch getan in der Klasse TestRunner. Was in der methode __call__ (soweit ich Python verstehe ist wird diese methode aufgerufen, wenn man die Klasse selbst wie eine methode benutzt, also “TestRunner()”) steht definiert einen Run. Hier werden in jedem Run die Tests “Google” und “Bing” jeweils 3 mal ausgeführt. Pro Run landen also 6 Einträge im Logfile. Nochmal zur Erinnerung: Jeder dieser Einträge setzt sich aus der Summe von 2 Requests (Startseite und Suche) zusammen.

Zusammenfassend sollten bei dieser Konfiguration 1 Agent * 1 Worker * 2 Threads * 5 Runs * 6 Tests * 2 Requests = 120 Requests gesendet werden, 60 auf Google und 60 auf Bing. Wem das zu viel ist, der kann ja die Konfiguration anpassen. An sonsten: Los! (Button in der Grinder-Konsole klicken, OK klicken, denn wir wollen die Konfiguration aus grinder.properties nehmen)

Es ist sinnvoll, dabei den Agent im Auge zu behalten. Nach Ablauf der Tests sieht es (bei einer lausigen Internetverbindung) etwa so aus:

GrinderTestsRun

Ich komme nicht ganz auf 60 Tests, da bei einer Exception (hier wahrscheinlich wegen Timeout) immer der ganze Run abgebrochen wird. Die Spalte “Response Bytes per Second” ist übrigens Unsinn, da dort einfach die Summe von ResponseLength durch die Gesamtlaufzeit dividiert wird.

Auswertung

Es lohnt sich, nun einen Blick in die entstandenen Logfiles zu werfen. Das File, dessen Nama mit “data” beginnt, enthält alle gesammelten Testergebnisse. Es ist im CSV-Format und kann daher einfach in ein Tabellenkalkulationsprogramm oder ein eigenes Auswertungs-Tool geladen werden. Es ist auch möglich selbst definierte Werte in das Logfile zu schreiben. Außerdem lässt sich der Lasttest auch automatisch über einen Continuous-Integration-Server wie TeamCity fahren. In TeamCity kann man dann sogar einen selbst definierten Lasttest-Report einbinden. Wie das alles funktioniert, werde ich in späteren Blogposts beschreiben.

Bis dahin: Happy Coding

Stefan Noack von Speak-Friend

enjoyed the post?

Tags:

Comments are closed

About Oliver

shades-of-orange.com code blog logo I build web applications using ASP.NET and have a passion for javascript. Enjoy MVC 4 and Orchard CMS, and I do TDD whenever I can. I like clean code. Love to spend time with my wife and our children. My profile on Stack Exchange, a network of free, community-driven Q&A sites

About Anton

shades-of-orange.com code blog logo I'm a software developer at teamaton. I code in C# and work with MVC, Orchard, SpecFlow, Coypu and NHibernate. I enjoy beach volleyball, board games and Coke.