Suche
Lasttests mit Gatling

Mit dem Testen von Anwendungen ist es so eine Last. Erst recht mit Lasttests!

Niko Köbler
nko

Niko Köbler

Mit einer Vorstellung des Load Testing Frameworks Gatling startet Niko Köbler (www.n-k.de, Twitter: @dasniko) in seine neue Kolumne auf JAXenter. Hier geht es in den nächsten Wochen um Webentwicklung mit Java, JavaScript, Nashorn und Co. Sein Fazit dieses Beitrags: “Mit Gatling gibt es keine Ausreden mehr, Lasttests nicht durchzuführen.”

Lasttests mit Gatling

Haben sich Unit-, Integrations- und Oberflächentests in der Vergangenheit doch mehr oder weniger gut durchgesetzt, fristen die Applikations-Lasttests eher ein Nischen-Dasein oder fallen meist Zeit- und damit auch Geld-Budget zum Opfer. In vielen Unternehmen gleichen Lasttests eher Smoke-Tests mit mehreren Mitarbeitern der Fachabteilung, die schnell aber unkontrolliert und wild hin und her klicken. Diese Tests haben keine Aussagekraft, sind nicht reproduzier- und damit nicht vergleichbar. Wenn ein Performance-Engpass auftritt, weiß man nicht, wer was geklickt hat und welche Aktion den Engpass auslöste.

JMeter ist hier der altbekannte Platzhirsch am Markt. Jedoch ist das Tool nicht nur altbekannt, sondern einfach auch alt – was so viel heißen soll wie “nicht mehr zeitgemäß”, angefangen bei der technologischen Basis, über das UI (altbackene Swing-Oberfläche) bis hin zum Datenformat. Dieser XML-Monolith der Test-Definition ist nicht mehr beherrsch- bzw. wartbar, erst recht nicht bei der Entwicklung im Team. Ist erst einmal ein XML-Knoten durch ein Merge-Conflict zerstört, wird das Flicken zum Alptraum.

img1

Gatling – In Scala geschrieben, aber kein Scala Know-how notwendig

Gatling schickt sich seit einiger Zeit an, dieses Feld neu aufzurollen. Mit einer zeitgemäßen Architektur basierend auf Scala, Akka und Netty arbeitet Gatling asychron und nicht-blockierd und damit mit einem hohen möglichen Durchsatz. Ich hatte in meinem letzten Projekt die Möglichkeit, mir Gatling genauer anzuschauen und dies für die Lasttests einzusetzen.

Bei dem Wort Scala zuckt der eine oder andere vielleicht noch zusammen, aber ich kann hier beruhigen. Denn obwohl Gatling in Scala implementiert ist, ist es nach außen vielmehr eine auf Scala basierende DSL. Der interessierte Anwender muss also nicht erst Scala lernen, um Gatling einsetzen zu können. Außerdem hilft die Code-Completion in der IDE sehr gut, die DSL anzuwenden.

Um mit Gatling zu starten, benötigen wir zunächst die Bibliotheken in unserem Test-Projekt. Dies erreichen wir duch die Angabe der Highcharts-Bibliothek, die eine transitive Abhängigkeit auf die Framework-Bibliothek mitbringt:

io.gatling.highcharts:gatling-charts-highcharts:2.1.7

Danach legen wir uns die Klasse `LoadScenario.scala` an…

    class LoadScenario extends Simulation {}

…, in die wir nun nacheinander die folgenden Code-Beispiele einfügen. In meinem GitHub Demo Repository ist das Beispiel auch zu finden.

Browser Simulation

Gatling ist erst mal eine Browser-Simulation, auch wenn Gatling sich natürlich nicht zum Darstellen von HTML-Seiten eignet und auch JavaScript-Code in einer HTML-Seite nicht ausführt/auswertet. Gatling ist aber nicht irgendein Browser, sondern verhält sich nach Außen so, wie man möchte und wie man Gatling über entsprechende Header konfiguriert.

    val httpConf = http
      .baseURL("http://localhost:8080")
      .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
      .doNotTrackHeader("1")
      .acceptLanguageHeader("en-US,en;q=0.5")
      .acceptEncodingHeader("gzip, deflate")
      .userAgentHeader("Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0")

Unsere Http-Konfiguration enthält eine Basis-URL, auf der alle weiteren Requests aufsetzen, ein paar Header-Informationen, welche Requests unser “Browser” akzeptiert und zum Ende die Angabe des “Agents”, d.h. als welcher Browser sich Gatling zum Server hin ausgibt. In unserem Falle wollen wir ein etwas älterer Firefox sein. Wir können so also problemlos auch mehrere Konfigurationen erstellen und diese gegeneinander testen, falls wir evtl. Bedenken haben, dass unsere Anwendung im Internet-Explorer eine deutlich schlechtere Performanz an den Tag legt als in einem Firefox. Wer weiß, was die Browser-Bridge auf dem Server von unserem Kollegen wirklich macht…

Schon dieses erste Code-Beispiel zeigt, wie einfach aber doch sehr verständlich die Scala-DSL von Gatling gehalten ist. Den gesamten Sprachumfang der DSL zeigt das Gatling Cheat Sheet in einer Übersicht.

Gatling kann aber nicht nur Browser sein und HTTP Requests testen, Gatling kann sogar eine WebSocket-Verbindung zum Server aufmachen und mit Server-Side-Events (SSE) umgehen! JMS Requests sind ebenso testbar wie auch, über 3rd-Party Plug-ins, Cassandra, MQTT, Kafka und RabbitMQ-Protokolle (für die 3rd-Party Plug-ins übernimmt Gatling im Moment zumindest aber noch kein Support).

Test-Szenario definieren

Ein Test-Szenario besteht aus (mindestens) einem oder mehreren Requests. Ein HTTP-Request kann mit allen bekannten Verben aufgebaut und um alle möglichen Optionen (Query-Parameter, Header, etc.) erweitert werden. Auch für den Aufbau eines Bodys stehen genügend DSL-Befehle zur Verfügung. Für die spätere leichte Auffindung unseres Szenarios und der einzelnen Requests in den Reports, können diesen Namen vergeben werden:

    val scn = scenario("BasicSimulation")
      .feed(csvFeeder)
      .exec(http("request_1")
      .get("/").queryParam("${key}", "${value}"))

Feeder

In obigem Code-Beispiel ist ein Feeder angegeben (.feed(csvFeeder)). Diesen Feeder haben wir bislang noch nicht definiert. Was ist ein Feeder überhaupt?

Feeder sind eine schöne, einfache Art, (variable) Daten von außen in den Test einzubringen. Dies können z.B. Daten für Query-Parameter, für zu übermittelnde Formulare oder andere Möglichkeiten sein. Gatling nimmt uns hier die aufwendige Konfiguration der Verwendung der Daten-Ressourcen ab und stellt eine ganze Reihe von vordefinierten Feedern zur Verfügung:

  • csv, tsv, ssv: Dateien mit Komma-, Tabulator- oder Semikolon-getrennten Werten
  • jsonFile, jsonUrl: Daten über eine JSON-Datei oder über eine JSON-Url beziehen
  • jdbcFeeder: Daten aus einer relationalen Datenbank mittels JDBC auslesen
  • redisFeeder: Daten aus einem Redis Key-Value-Store beziehen

Der Feeder kümmert sich darum, dass die Verbindung zur Ressource hergestellt wird und die Daten vor der Testausführung eingelesen werden. Die Verwendung der in der jeweiligen Ressource zur Verfügung stehenden Datensätzen wird über drei verschiedene Arten ermöglicht:

  • queue (default): die Daten werden nacheinander verwendet, wie in einer Queue. Wenn die Daten aufgebraucht werden, bevor der Test zu Ende ist, wird eine Exception geworfen
  • curcular: ähnlich wie queue, jedoch wird am Ende der Daten wieder von vorne begonnen
  • random: wie der Begriff schon vermuten lässt, wird per Zufallsprinzip auf die Datensätze zugegriffen

Unseren Feeder definieren wir mit…

    val csvFeeder = csv("values.csv").circular

… und injizieren die Werte mittels der Expression Language über ${key} und ${value} in die Query-Parameter ein. Bei CSV-Daten werden die Feldbezeichner aus der ersten Zeile der Datei als Variablennamen übernommen.

Test-Setup – Die Einzelteile zusammenfügen

Unsere Http-Konfiguration und das Test-Szenario können wir nun zu einem Test-Setup zusammenfügen und ausführen. Damit Gatlin weiß, mit wievielen Benutzern gleichzeitig getestet werden soll, muss dies in diese Information in das Test-Szenario eingefügt werden. Die einfachste Weise ist atOnceUsers(n) anzugeben, damit werden die angegebene Anzahl Benutzer gleichzeitig erzeugt und für diese das Test-Szenario ausgeführt.

Das reicht aber meist nicht, und deshalb gibt es weitere Möglichkeiten, die hier injected werden können, so z.B. eine ansteigene Anzahl von Usern: rampUsers(500), oder auch eine gleichbleibende Anzahl User über eine Zeit (hier 1000 User über 10 Sekunden): constantUsersPerSec(1000) during (10).

Außer den o.g. stehen noch viele weitere Optionen zur Verfügung. Alle Möglichkeiten sind im Cheat Sheet aufgeführt.

Weiterhin müssen wir angeben, dass das Set-up unsere bestehende Http-Konfiguration verwendet. Alles weitere ist optional:

    setUp(
      scn.inject(atOnceUsers(1))
    )
      .protocols(httpConf)
      .assertions(
        global.responseTime.max.lessThan(50),
        global.successfulRequests.percent.greaterThan(95)
      )

Assertions

Wie in obigem Code zu sehen ist, können auch Assertions an die Testausführung angehängt werden. D.h. wir können bereits Erfolgskriterien angeben, bevor überhaupt ein Test gelaufen ist und bekommen direkt nach der Testausführung das Feedback, ob die einzelnen Assertions, und damit der Gesamt-Lasttest, erfolgreich war oder nicht. Denn was bringt es denn, wenn wir aufwändig jeden einzelnen der 1000 Ausführungen überprüfen müssen, ob die Ausführung in Ordnung war oder nicht. Schnelles Feedback ist wichtig. Ist der Lasttest in unseren angegebenen Grenzen “grün”, müssen wir nicht mehr unbedingt die Details überprüfen und sparen uns so wichtige Zeit. Sind die Erwartungen jedoch nicht erfüllt, können wir uns der detaillierten Fehlersuche widmen.

In unserem Beispiel ist es in Ordnung, wenn die Antwortzeit unter 50ms liegt und wir mindestens 95% erfolgreiche Requests absetzen können (und wir eine valide Antwort dazu erhalten). Sind mehr als 5% nicht erfolgreich (keine 200er Response Status Codes) oder liegen einzelnen Requests über 50ms, so gilt unser Lasttest als gescheitert.

Selbstverständlich sind die Assertions optional und können einfach weggelassen werden, wenn diese nicht benötigt oder nicht gewünscht werden.

Ausführung

Für die Ausführung der Tests bietet sich die Verwendung von SBT (“Scala-Build-Tool”) an. Jedoch sind aber auch Plugins für andere Build-Systeme (wie z.B. Maven & Gradle) verfügbar. Diese stammen aber teilweise von Drittanbietern und sind nicht offiziell von Gatling supportet (s.o.).

Ein kurzer Aufruf von

sbt test

genügt, und der Lasttest wird ausgeführt. Auf der Konsole werden jetzt jede Menge Informationen ausgegeben, die man am Anfang nicht wirklich zu verstehen braucht. Lediglich die Ausgabe am Ende des Tests sind von Beginn an interessant, werden hier doch die aggregierten Ergebnisse des Tests ausgegeben. Ganz schlicht und simpel bedeutet ein “OK”, dass der Test erfolgreich war und ein “KO” eben das Gegenteil.

Wer reine Kommandozeilen-Aufrufe mag, findet auch Shell- und Batch-Scripte für die Test-Ausführung. Ein dritter Weg, die Gatling-Tests starten zu können, ist der direkt aus der IDE heraus. Hierfür sind zwei kleine Helferklassen notwendig, die zum einen die Umgebungspfade definieren (IDEPathHelper.scala) und zum anderen eine ausführbare App-Klasse, die ein Scala-Programm darstellt (Engine.scala). Die zwei Klassen sind im [GitHub Beispiel-Repository][github] zu finden.

Reports

Gatling erzeugt standardmäßig mehr oder weniger schöne HTML-Reports. Ob die verwendete Farbgebung, ein auffallendes Orange, jedermanns Sache ist, bleibt dahingestellt. Was aber wirklich nicht von der Hand zu weisen ist, ist die Aussagekraft der Reports. Von einer sehr globalen Übersicht und Zusammenfassung der Testergebnisse, bis hin zu detaillierten Laufzeitverhalten einzelner Requests, kann man sich alle Informationen aus den Berichten rausholen. Auch schon auf der Kommandozeile bei der Ausführung der Gatling-Tests ist einiges an Informationen zu lesen, was aber in den HTML-Reports deutlich besser aufbereitet ist.

gatling_report_1

Gatling Report Overview

 

gatling_report_2

Gatling Report Request-Detail

 

Die in unserem Test-Code verwendeten Bezeichner BasicSimulation (für das Szenario) und request_1 (für den konkreten HTTP-Request) finden sich im Bericht wieder, somit ist eine einfache und schnelle Zuordnung der Testergebnisse möglich. Die beiden Screenshots sollen hier als Beispiel für die Aussagekraft genügen. Mit der Ausführung von eigenen Tests werden die Berichte sehr schnell sehr gut verständlich!

Test-Recorder

Nun ist so eine kurze Testklasse für einen einzelnen Request schnell geschrieben. Meist werden jedoch mehrere unterschiedliche Requests nacheinander ausgeführt, was eine manuelle Nutzung der Applikation durch viele Anwender simulieren soll. All diese Requests manuell zu programmieren, wäre sicherlich nicht im Sinne des Erfinders. Deswegen gibt es auch für Gatling einen Test-Recorder, der als Plug-in in den Browser gehängt werden kann und nach Start alle ausgeführten Requests aufzeichnet, inkl. der vom Benutzer gemachten Pausen während der Aufzeichnung!

gatling_recorder

Gatling Recorder

 

Aus dieser Aufzeichnung generiert der Recorder dann den Scala-DSL-Code. Wer Angst hat, dass der generierte Code nicht lesbar und damit nur schwer und/oder eingeschränkt weiter verwend- und anpassbar ist, dem darf gesagt sein, dass der generierte Code überraschend gut lesbar und nicht weiter verschachtelt erzeugt wird. Ich war selbst überrascht, wie gut das funktioniert.

Viel mehr ist über den Recorder aber auch nicht zu sagen. Es gibt ihn, er funktioniert, macht was er soll und das gut. Mit dem erzeugten Code kann dann weitergearbeitet werden: in die Versionskontrolle einchecken, evtl. anpassen und möglichst oft automatisiert ausführen, so dass Bottlenecks möglichst früh erkannt werden können.

Fazit

Mit Gatling gibt es nun keine Ausreden mehr, Lasttests nicht durchzuführen. Mit Gatling wird es sogar super einfach, Lasttests schon von Projektbeginn an in der Build-Pipeline zu integrieren und auszuführen. Da die Test-Szenarien in Form von Code (Gatling Scala-DSL) vorliegen und gleichzeitig mit Code-Änderungen in der zu testenden Anwendung angepasst werden können, wird so ein später Performance-Engpass vermieden. Mit Assertions kann der grobe Rahmen für erfolgreiche Testläufe festgelegt werden, diese Ergebnisse sind schnell ersichtlich. Details können in den ausführlichen Reports nachgelesen werden.

Geschrieben von
Niko Köbler
Niko Köbler
Niko Köbler ist freiberuflicher Software-Architekt, Developer & Trainer für Java & JavaScript-(Enterprise-)Lösungen, Integrationen und Webdevelopment. Er ist Co-Lead der JUG Darmstadt, schreibt Artikel für Fachmagazine und ist regelmäßig als Sprecher auf internationalen Fachkonferenzen anzutreffen. Niko twittert @dasniko.
Kommentare

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht.