Einen automatisierten Lasttest mit JMeter durchführen

Motivation

Ich habe mich gefragt, wie sich eine von mir entwickelte Webanwendung unter Last verhält. Mit Last meine ich, dass mehrere Benutzer gleichzeitig auf die Anwendung zugreifen. Diese Situation kann ich alleine nicht manuell nachstellen, und auch den Ansatz, 10 Leute zusammenzutrommeln und gleichzeitig die Anwendung zu bedienen, halte ich für unprofessionell. Ein Lasttest ist mich primär ein automatisierter Test.

Mir schwebt vor, eine umfangreiche Abfolge von Benutzerinteraktionen zu simulieren, die dem kompletten Lebenszyklus eines Anwenders entsprechen, also von der Registrierung des Anwenders bis zum Löschen seines Benutzerkontos. Das will ich dann mehrfach und parallel abspielen und so eine bestimmte Anzahl gleichzeitiger Zugriffe auf die Anwendung nachstellen.

Da ich auf diesem Gebiet keine Erfahrungen habe, recherchiere ich zunächst, welche Tools dafür in Frage kommen.

Tools

Eine Recherche nach Tools gestaltet sich wie so häufig: Der eine findet das besser, der andere das, und alle kommen zu dem Schluss, dass es von den Anforderungen abhängt, welches Tool das beste ist. So kann ich ich auch keine Empfehlung aussprechen, sondern nur beschreiben, wie ich zu meiner Entscheidung komme.

Cloud oder nicht Cloud

Bei der Rechereche bin ich schnell auf Anbieter gekommen, die die Testinfrastruktur als SaaS in der Cloud bereitstellen. Das klingt zunächst interessant, da keine Installation erforderlich ist und ein solcher Dienst den Schwerpunkt auf Webanwendungen hat. Anbieter sind:

Vielleicht tue ich diesen Tools unrecht, aber ich habe mich schnell gegen sie entschieden. Alle Tools sind kostenpflichtig, gratis ist nur ein abgespeckter Funktionsumfang. Meist wird in der Gratis-Variante die Anzahl der Testminuten oder Testzugriffe begrenzt. Da ich aber noch keine Erfahrungen habe, gehe ich davon aus, dass ich auch Zeit und Zugriffe benötige, um zu lernen. Ich befürchte, dass ich im schlimmsten Fall mein Gratis-Budget aufgebraucht habe, bevor der Test startet.

Außerdem möchte ich erstmal meinen lokalen Server unter Dampf setzen. Das geht aus der Cloud auch nicht so problemlos.

Python oder Nicht-Python

Zwei Tools, die im Zusammenhang mit Lasttests häufig erwähnt werden, sind Pylot und der Quasi-Nachfolger Multi-Mechanize. Beide basieren auf Python. Ich habe keine Erfahrungen mit Python. Er wäre reizvoll, diese Erfahrungen zu machen und anzufangen, die Sprache und die Python-Landschaft kennenzulernen. Ich traue mich am Ende aber doch nicht, da die Zeit, die ich in das Thema stecken möchte, begrenzt ist.

JMeter oder The Grinder

JMeter und The Grinder werden gern verglichen. Nach kurzer Recherche komme ich zu dem Schluss, es erstmal mit JMeter zu versuchen. Mit Software aus der Apache-Community habe ich bisher immer gute Erfahrungen gemacht. In JMeter kann ich außerdem den Test mit einer Benutzeroberfläche zusammenstellen, während The Grinder mit Testskripten arbeitet. Da ich erst Erfahrungen mit Lasttests sammeln möchte, halte ich das für einen Vorteil. Ich will erstmal nur mit der Benutzeroberfläche arbeiten, auch wenn JMeter zusätzlich per Kommandozeile steuerbar ist.

Installation

Ich lade JMeter in Version 2.9 herunter und packe das zip aus. Ich arbeite mit OSX und starte jmeter aus dem Terminal heraus. Zuvor muss ich noch die Datei jmeter mit

chmod +x jmeter

ausführbar machen.

Es wird empfohlen, sich Gedanken um den verfügbaren Speicher für JMeter zu machen und gegebenfalls die Parameter -Xms und -Xmx beim Aufruf zu übergeben. Ich mache das nicht, ich starte jmeter also out-of-the-box.

Manual, How-to, Guide

Die wichtigste Quelle ist die offizielle Seite von JMeter (http://jmeter.apache.org/). Dort nutze ich vor allem die Menüpunkte User Manual, Best Practices, Component Reference und Functions Reference. Das User Manual ist für den Einstieg gut geeignet.

Es gibt auch einige hilfreiche Blogs im Netz, z.B. http://lincolnloop.com/blog/load-testing-jmeter-part-1-getting-started/.

Sprache

Die Anwendung ist lokalisiert und startet in deutscher Sprache. Manche Begriffe sind aber etwas unglücklich übersetzt, so heißt beispielsweise eine Response Assertion im Deutschen Versicherte Antwort. Ich komme mit den englischen Begriffen besser zu recht und schalte die Sprache um. In den folgenden Kapiteln verwende ich die englischen Begriffe.

Test Plan und Thread Group

Und los geht's. Ich möchte meinen lokalen Server mit Testaufrufen befeuern. Auf dem Server läuft eine Anwendung die sowohl ganze HTML-Seiten zurückliefert als auch JSON über eine REST-API. Beides wird von JMeter unterstützt.

Ich erstelle einen Test Plan und darin eine Thread Group. In der Maske des Test Plan lege ich einige Variablen fest, die für den gesamten Test gelten. Diese kann ich an fast allen Stellen über ${<Name der Variablen>} verwenden.

jmeter_test_plan

In der Thread Group lege ich fest, wie viele Threads den Test durchführen sollen und wie häufig das passieren soll. Ich lasse die Werte erstmal so wie sie sind (=1), denn mein erstes Ziel ist ein erfolgreicher Durchlauf. Erst wenn der Test stabil durchführbar ist, möchte ich (und sollte ich) diese Werte anpassen.

Darstellung der Ergebnisse

Um die Ergebnisse des Tests sehen zu können, sind Listener erforderlich. Ich füge sie als erste Kind-Elemente der Thread Group ein.

Die Position der Listener ist nicht relevant, ich versuche aber ein bisschen Ordnung zu halten. Unterhalb der Thread Group liegen zuerst alle Listener, ein User Parameters Element, dann Konfigurationselemente und schließlich die Sampler, die die einzelnen Testschritte repräsentieren.

jmeter_elements

Für den Anfang ist Save Responses to a file wichtig. Anhand von ein paar Dateien (z.B. zuletzt erhaltener Response als HTML) bekommt man hilfreiche Anhaltspunkte, wenn der Test nicht durchläuft. Als Filename prefix kann man einen Pfad eingeben und somit das Verzeichnis festlegen, in dem die Dateien angelegt werden. Ich aktiviere zudem Don’t add number to prefix, damit die Testergebnisse immer in dieselbe Datei geschrieben werden.

Es wird empfohlen, dieses Element beim richtigen Testdurchlauf zu deaktivieren, um die Performance des testenden Systems nicht zu beeinträchtigen.

Obligatorisch für mich ist der Knoten Assertion Results. Hier werden Assertions und eventuelle Verletzungen geloggt. Assertions sichern ab, dass der Test auch so durchläuft, wie man es erwartet, also meist erfolgreich (z.B. mit Response Code 200) oder mit bestimmtem Inhalt.

Es gibt einige Listener, die die Testergebnisse darstellen. Ich probiere ein paar aus. Sie stellen die Ergebnisse in Diagramm-, Tabellen oder Baumform dar. Welche die richtige Form ist, muss jeder selbst entscheiden.

User Parameters

Benutzerdefinierte Variablen wie sie über den Test Plan oder das Element User Defined Variables festgelegt werden, gelten für alle Threads. Falls es erforderlich ist, eine Variable pro Thread bzw. pro Durchlauf festzulegen, ist das Element User Parameters zu verwenden.

Ich verwende es, um eine E-Mail-Adresse zu generieren, die im weiteren Testverlauf verwendet wird. Hier verwende ich mit __threadNum() und __Random() zwei Funktionen, die JMeter bereitstellt.

jmeter_user_parameters

Konfigurationselemente

HTTP Request Defaults ist hilfreich, um Default-Werte für die Ausführung von HTTP-Requests festzulegen, z.B. Port, Host und Port. Ich verwende dabei die Variablen, die ich für den Test Plan gesetzt habe.

jmeter_http_request_defaults

Den HTTP Cookie Manager füge ich ein, ohne ihn zu konfigurieren. Er ist wichtig, wenn ein Login im Test stattfindet oder Daten in einer Benutzersession liegen.

Den HTTP Header Manager setze ich ein, um Header zu setzen, die generell übermittelt werden sollen. Um zwischen Requests, die HTML und JSON zurückgeben, zu unterscheiden, kann man den HTTP Header Manager auch (zusätzlich) für einzelne Aufrufe unterhalb eines HTTP Request Elements einfügen.

jmeter_http_header_manager

HTTP Requests

Ab jetzt arbeite ich nur noch mit HTTP Request Samplern und ein paar hilfreichen Kindelementen. Für einen Request gebe ich den Pfad und die Methode (GET/POST/PUT/DELETE) ein. Den Rest erledigen die oben gesetzten Default-Werte.

Üblicherweise setze ich Post-Parameter über Name-Wert-Paare. Im Falle von JSON-Requests ist es manchemal erforderlich, im Tab Post Body einen JSON-Ausdruck einzufügen.

jmeter_http_request_post_body

Auch wenn ich die zu testende Anwendung selbst geschrieben habe und den Code verfügbar habe, finde ich es einfacher, mit Firebug zu schauen, wie die Requests vom Browser gesendet werden und wie die Responses aussehen.

Response Assertions

Ein Test ohne Assertion ist nicht sinnvoll. Bei einem erfolgreichen Testdurchlauf beeinflusst eine Assertion den Ablauf zwar nicht, aber dann weiß ich nicht, ob nicht einzelne Aufrufe in einem Fehler münden (z.B. Darstellung einer Fehlerseite oder Response Code 500). Daher: Immer eine Assertion als Kind-Element eines HTTP Request einsetzen.

Eine einfache Assertion ist die Überprüfung des Response Codes, meist 200 (ok) oder 201 (created)

 jmeter_response_assertion_response_code

Bei HTML-Responses ist die Prüfung mit der Contains-Regel sinnvoll. Man überprüft damit, ob ein bestimmter Inhalt im Response enthalten ist.

 jmeter_response_assertion_contains

Bei JSON-Aufrufen kann man sogar noch konkreter werden. Ich erwarte an einer Stelle eine leere Liste und kann das mit Equals prüfen

 jmeter_response_assertion_equals

Ich merke irgendwann, dass der Test auch bei fehlerhaften Assertions durchläuft. Das liegt an der Default-Einstellung Action to be taken after a sampler error der Thread Group. Ich setze diese auf Stop Thread und ab jetzt bricht ein Thread ab, wenn eine Assertion verletzt wird.

Extrahieren von Daten aus dem Request

Weitere hilfreiche Elemente sind Extraktoren (Post Processor). Ich setze XPath Extractor sowie Regular Expression Extractor ein. Mithilfe dieser Elemente lassen sich Informationen aus dem Response herauslesen und in JMeter-Variablen speichern. Ein Beispiel ist die serverseitig vergebene Id für ein neu angelegtes Element. Die Variable kann dann in Folgeaufrufen verwendet werden.

Reguläre Ausdrücke und XPath sind nicht mein Steckenpferd. Daher gehe ich zunächst auf Internetseiten, auf denen ich Ausdrücke ausprobieren kann, bevor ich meinen Test ausführe, z.B. Rubular für reguläre Ausdrücke und Simple online XPath tester.

Debug Sampler

Gerade im Zusammenhang mit den Extraktoren ist der Debug Sampler hilfreich, mit dem sich aktuelle Variablen ausgeben lassen. Wenn ich unsicher bin, ob Variablen richtig gesetzt werden, setze ich diesen hinter den Extraktionsschritt. Wichtig: Save Responses to a file muss aktiv sein, damit die Variablen geschrieben werden.

Logic Controller

An einer Stelle verwende ich einen Loop Controller, um Aktionen in einer Schleife auszuführen, sammle ansonsten aber wenig Erfahrungen mit Logic Controller Elementen.

Trial and Error und weiteres Vorgehen

Ich gehen nach dem Trial-and-Error-Prinzip vor. Also immer wieder ausführen und Fehler suchen. Das ist meiner Ungeduld geschuldet, denn ich will schnelle Ergebnisse und auch schnell wissen, ob JMeter weiterhin das richtige Tool ist. Mal mache ich dabei gute Fortschritte, mal halte ich mich länger mit der Fehlerbehebung auf.

Die Bedienoberfläche von JMeter ist manchmal etwas altbacken und umständlich, aber am Ende doch leicht zu begreifen.

Wie eingangs kurz erwähnt, setze ich den Test so um, dass der gesamte Lebenszyklus eines Benutzers abgehandelt wird. Von der Registrierung über das Arbeiten mit dem System bis zum Löschen des Benutzerkontos. Das hat den Vorteil, dass der Test selbstständig ohne manuelle Vorarbeiten (z.B. Anlage von Testusern) läuft und am Ende sogar noch aufräumt, also auch keine Nacharbeiten notwendig sind.

Da der Versand eines Bestätigungslinks via E-Mail ein Bestandteil der Registrierung ist, gehe ich den Weg über die Administrationsoberfläche des Systems. Diese rufe ich auch über Testschritte auf und kann so den Mail-Versand umgehen.

Nachdem ich den kompletten Ablauf fertig konfiguriert habe, setze ich die Anzahl der Threads hoch und schaue, ob der Test immer noch durchläuft. Nachdem ich mir sicher bin, dass mein Test gut ist, und auch bei den Einstellungen zu den Threads sicher bin, schwenke ich von meiner lokalen Umgebung auf die Produktion um.

Errors

Zweimal halte ich mich länger auf, als ein Error mit Java-Stacktrace im Log erscheint.

java.lang.InterruptedException

WARN  - jmeter.util.JMeterUtils: Interrupted in thread Thread Group 1-3 java.lang.InterruptedException
   at java.lang.Object.wait(Native Method)
   at java.lang.Object.wait(Object.java:485)
   at java.awt.EventQueue.invokeAndWait(EventQueue.java:1121)
   at java.awt.EventQueue.invokeAndWait(EventQueue.java:1103)
   at javax.swing.SwingUtilities.invokeAndWait(SwingUtilities.java:1326)
   at org.apache.jmeter.util.JMeterUtils.runSafe(JMeterUtils.java:1296)
   at org.apache.jmeter.visualizers.AssertionVisualizer.add(AssertionVisualizer.java:60)
   at org.apache.jmeter.reporters.ResultCollector.sendToVisualizer(ResultCollector.java:525)
   at org.apache.jmeter.reporters.ResultCollector.sampleOccurred(ResultCollector.java:501)
   at org.apache.jmeter.threads.ListenerNotifier.notifyListeners(ListenerNotifier.java:84)
   at org.apache.jmeter.threads.JMeterThread.notifyListeners(JMeterThread.java:783)
   at org.apache.jmeter.threads.JMeterThread.process_sampler(JMeterThread.java:442)
   at org.apache.jmeter.threads.JMeterThread.run(JMeterThread.java:256)
   at java.lang.Thread.run(Thread.java:680)

Dieser Fehler trat auf, da ich die Einstellung Action to be taken after a Sampler error auf Stop Test Now gesetzt hatte. Dann werden alle Test-Threads direkt beendet, sobald ein Thread auf einen Fehler stößt, was zu dieser Exception führt. Sinnvoller ist die Einstellung Stop Thread oder Stop Test (ohne now)

javax.net.ssl.SSLPeerUnverifiedException

javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
   at sun.security.ssl.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:397)
   at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:126)
   at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:572)
   at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:180)
   at org.apache.http.impl.conn.ManagedClientConnectionImpl.open(ManagedClientConnectionImpl.java:294)
   at org.apache.http.impl.client.DefaultRequestDirector.tryConnect(DefaultRequestDirector.java:645)
   at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:480)
   at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:906)
   at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:805)
   at org.apache.jmeter.protocol.http.sampler.HTTPHC4Impl.sample(HTTPHC4Impl.java:286)
   at org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy.sample(HTTPSamplerProxy.java:62)
   at org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase.sample(HTTPSamplerBase.java:1088)
   at org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase.sample(HTTPSamplerBase.java:1077)
   at org.apache.jmeter.threads.JMeterThread.process_sampler(JMeterThread.java:428)
   at org.apache.jmeter.threads.JMeterThread.run(JMeterThread.java:256)
   at java.lang.Thread.run(Thread.java:724)

Dieser Fehler tritt auf, als ich auf meine produktive Anwendung, die nur per HTTPS erreichbar ist, teste. Ich google nach dem Fehler, importiere ein Zertifikat in meinen Java keystore und aktualisiere mein JDK auf Version 7. Der Fehler tritt immer noch auf und am Ende bin ich der Depp, da ich Port 80 eingestellt habe, der Standard-Port für HTTPS aber 443 ist. Ein unnötiger Fehler, da JMeter die Standard-Ports kennt, ich lasse den Port also leer und der Fehler verschwindet.

Ich kann aber nicht ausschließen, dass auch die Schritte keystore-Import und JDK-Update dazubeigetragen haben, dass der Fehler verschwindet.

Interpretation der Ergebnisse

Da die Testergebnisse von JMeter immer angehängt werden, also die Ergebnisse mehrerer Testläufe in einem Diagramm, einer Tabelle oder einem Baum landen, ist der Clear oder Clear All Button wichtig. Denn gerade, wenn man noch mit den Parametern wie Anzahl Threads etc. spielt, sind zwei Testläufe vollkommen unterschiedlich. So sähe das dann aus (irgendwie künsterlisch):

jmeter_graph_result

Die Interpretation der Ergebnisse finde ich schwierig. Die Diagramme sind schwer zu interpretieren, am besten komme ich mit Tabellen zurecht. Aber auch hier tue ich mich schwer. Ich frage mich, wie ich am besten den Bezug zur Realität herstelle. Mein Test führt den Lebenszyklus eines Benutzers in einer Sekunde aus, für den der Benutzer mindestens eine Stunde braucht. Auch werden nicht alle Benutzer meiner Anwendung gleichzeitig online sein und gleichzeitig Knöpfe und Tasten drücken.

Ich gehe am Ende so vor. Ich lasse 15 Threads (Ramp up in 5 Sekunden) 15 Mal den Test ausführen. Zum einen schaue ich mir am Ende das Testergebnis an. Dabei sind die Maximalwerte am interessantesten, da diese am ehesten auf eine hohe Last hindeuten. Zum anderen gehe ich während des Tests hin, und bediene die Anwendung mit meinem eigenen Account manuell mit dem Webbrowser. Während also im Hintergrund 15 Fleißlinge alles dafür tun, die Anwendung zu beschäftigen, schaue ich, wie die Anwendung mich als 16ten bedient. Ich bin zufrieden, denn die Anwendung antwortet immer noch ausreichend schnell.

Fazit

Ich konnte am Ende in überschaubarem Aufwand den Test erstellen und ausführen. Da ich mich mit dem Lasttest nur grob absichern, also keine Wissenschaft daraus machen wollte, bin ich mit den Ergebnissen zufrieden.

Meine zu testende Anwendung hat eine Schnittstellen. Es mag sein, dass andere Anwendungen wesentlich schwerer zu testen sind. Ich denke dabei an JSF. Eventuell müssen hier sehr viele Daten aus Responses extrahiert werden (Ids von Formularen und Elementen) und bei einem Postback übertragen werden.

Am Ende enthält mein Test 25 aufeinanderfolgenden HTTP Requests. Sofern diese oder verwendete Assertions ähnlich oder identisch sind, habe ich diese kopiert. Mit anderen Worten, es gibt keine gute Unterstützung, Elemente in einer Art Bibliothek bereitzustellen um die dann aus dem Test heraus nur zu referenzieren (DRY-Prinzip). Auch besteht keine Möglichkeit mithilfe von Ordnern Testschritte zusammenzufassen und so seinen Test übersichtlicher zu gestalten.

Diese Nachteile habe ich aber in Anbetracht des Ergebnisses und des überschaubaren Aufwands gerne in Kauf genommen.

2017-01-28T13:52:53+00:00 01.09.2013|Tags: , , , , |4 Comments

4 Kommentare

  1. Tekl 1. September 2013 um 20:39 Uhr- Antworten

    Wow, da bist du ja richtig in die Tiefe gegangen.

  2. Johannes 3. September 2013 um 6:55 Uhr- Antworten

    Hi Bjoern,

    Ich musste bei einem meiner letzten Projekte auch performance tests mit JMeter einführen. Dabei habe ich eine recht gute Lösung fuer den Kritikpunkt in Deinem Fazit (keine Unterstützung fuer Bibliotheken) gefunden:
    Mit Hilfe von ruby-jmeter (https://github.com/flood-io/ruby-jmeter) habe ich unsere Tests einfach in Ruby geschrieben. Dadurch konnte ich wiederkehrende Requests oder Assertions sehr einfach einmal deklarieren und dann wiederverwenden. Auch wenn Du kein Ruby kennst, lässt sich mit der library gut arbeiten, weil die eine selbsterklärende DSL zur Verfügung stellt.
    Wenn Du willst, kann ich Dir mal ein wenig Beispiel-Code schicken.

    • Björn Weinbrenner 3. September 2013 um 22:51 Uhr- Antworten

      Hi Johannes!
      Der Hinweis ist gut für diejenigen, die JMeter ohne GUI steuern wollen. Ruby finde ich persönlich auch super und glaube dir sofort, dass man da leicht reinfindet. Ein Codebeispiel würde mich interessieren, du kannst es gerne hier posten.
      Viele Grüße
      Björn

  3. Johannes 4. September 2013 um 5:23 Uhr- Antworten

    Hier ist das Code-Beispiel:

    https://gist.github.com/jtuchscherer/28baa2347a068b7dcb04

    Der RequestHelper ist nicht ganz vollstaendig, aber das Beispiel sollte doch genuegen, um die Staerke von ruby-jmeter zu zeigen.

    Uebrigens kannst Du das von dem Code erzeugte jmx file immer wieder in der GUI anschauen. Das ist praktisch, wenn Du pruefen willst, ob das auch alles richtig ist.

Hinterlassen Sie einen Kommentar