Vaadin unter Last – Teil 1: Aufsetzen der Testumgebung

Vaadin ist eine moderne und gleichzeitig eine sehr reife Technologie zur Umsetzung von Web-Anwendungen in Java. Um im Unternehmens-Einsatz zu bestehen ist es unerlässlich, dass eine mit Vaadin implementierte Anwendung unter hoher Last zuverlässig arbeitet. Um den Einfluss von Vaadin auf die Performance einer damit umgesetzten Web-Anwendung zu evaluieren, hat die akquinet AG einen entsprechenden Test aufgesetzt. Im nun folgenden ersten Teil werden wir unser Test-Setup darstellen und den Umgang mit den verwendeten Tools, insbesondere JMeter, diskutieren. Im zweiten und dritten Teil dieser Blog-Serie werden wir auf die eigentlichen Testergebnisse eingehen.

Getestete Vaadin Anwendung

Was wir natürlich als Erstes benötigen, um die Performance von Vaadin zu messen, ist eine mit Vaadin implementierte Anwendung. Wir verwenden dafür den Officewerker, eine Anwendung zum Office-Management in kleinen Unternehmen. Unter anderem können mit der Anwendung Ein- und Ausgangsrechnungen verwaltet werden. Officewerker ist eine Entwicklung der akquinet AG; die Anwendung setzt auf den klassischen Java EE Stack mit Vaadin als UI-Technologie auf.

Officewerker UI
Officewerker UI

Die UI ist nach dem Model-View-Presenter Entwurfsmuster implementiert. Die Presenter halten dabei den aktuellen Zustand der Oberfläche. Unter der UI Schicht liegt die Schicht für die Geschäftslogik, die bis auf wenige Ausnahmen zustandslos mit Stateless Session Beans implementiert ist. In der Persistenzschicht werden Entitäten per JPA (EclipseLink) auf eine relationale Datenbank abgebildet. Als Datenbank verwenden wir PostgreSQL; als Applicationserver dient Glassfish.

Testumgebung

Um das Verhalten einer Web-Anwendung zu evaluieren, die mehrere hundert bis tausend Benutzer gleichzeitig bedienen soll, ist es gemeinhin sinnvoll, zwei Prämissen beim Aufsetzen der Umgebung zu befolgen:

  1. ein realistisches Sizing der zugrunde liegenden Hardware
  2. eine Trennung der Last erzeugenden Maschine von der zu testenden Maschine

Daher haben wir uns entschieden, den Test in der Amazon Elastic Computing Cloud auszuführen. Dadurch erhalten wir eine produktionsnahe Umgebung zu überschaubaren Kosten.

Sowohl der die Anwendung als auch der Testtreiber laufen jeweils auf einer Instanz in der Amazon EC2. Die ausgewählten Instanzen sind wie folgt ausgestattet:

Merkmal der Instanz Testtreiber (Client) Web-Anwendung
Type m1.large m1.xlarge
Archtektur 64 Bit 64 Bit
vCPU 2 4
EC2 Recheneinheiten 4 8
Arbeitsspeicher 7,5 GB 15 GB
Netzwerkleistung mittel hoch

Das Aufsetzen der beiden Instanzen stellte kein großes Problem dar und war Dank ausführlicher Dokumentation seitens Amazon schnell erledigt.

Ablauf des Tests

Der Test, den wir ausführen wollen, soll folgenden Ablauf haben:

  • Anmeldung an der Anwendung und Aufruf der Startseite
  • Wechsel in die Ansicht für Eingangsrechnungen
  • Eingabe eines Suchbegriffs, um aus der Liste aller Eingangsrechnungen nur eine anzuzeigen. Dieser Schritt wird mehrfach wiederholt.
  • Abmelden von der Anwendung

Vorbereiten der Anwendung für den Test

Für die Ausführung unseres Tests benötigen wir Testdaten in der Anwendung:

  • Zum einen benötigen wir Benutzer, damit ausreichend Zugänge für die parallele Anmeldung an der Anwendung vorhanden sind.
  • Zum anderen benötigen wir eine realistische Anzahl von Eingangsrechnungen, um realistische Last auf die Oberfläche und alle anderen Schichten zu bekommen.

Wir haben Insert-Skripts für die Erzeugung gewählt. Dabei war es wichtig, die Testdaten so anzulegen, dass diese in der späteren Testausführung leicht zu referenzieren sind. Dies wurde durch einen vollständig besetzten Nummernkreis erreicht – sprich: jede Zufallszahl wird auch als Rechnungsnummer gefunden.

Neben der Erzeugung der Testdaten, mussten wir auch in den Code bzw. die Konfiguration der Anwendung eingreifen:

  • Zum einen bietet Vaadin einen eingebauten Schutz gegen Cross Site Request Forgery (XSRF). Wenn man JMeter gegen eine Vaadin Anwendung ausführt, kann es sein, dass Vaadin dies als einen solchen Angriff wertet. Abhilfe schafft eine Einstellung in der web.xml:
    [sourcecode language=”xml”]
    <init-param>
    <param-name>disable-xsrf-protection</param-name>
    <param-value>true</param-value>
    </init-param>
    [/sourcecode]

    Man sollte allerdings unbedingt dafür sorgen, dass diese Einstellung wirklich nur für Testzwecke verwendet wird – XSRF gehört laut OWASP zu den Top Ten der häufigsten Attacken auf Web-Anwendungen.

  • Zum anderen ist es sinnvoll, die Elemente der UI mit einer dauerhaften ID zu versehen. Damit funktioniert das Aufzeichnen und das spätere Abspielen eines JMeter Testplans stabil. Vaadin bietet die Möglichkeit, für jede UI Komponente eine sogenannte DebugId zu vergeben, die sich im DOM des Browsers sowie in der Client-Server-Kommunikation wiederfindet. Dazu verwendet man die Methode setDebugId(String) des Interfaces com.Vaadin.terminal.Paintable. Dieses Interface wird praktisch von jeder Vaadin Komponente implementiert.
    Generell sollte das Vergeben von DebugIds eine grundsätzliche Regel in der Entwicklung sein, denn auch Oberflächentest mit der Vaadin Testbench oder Selenium laufen damit stabiler und effizienter. Die Vergabe wird im Folgenden dargestellt:
    [sourcecode language=”java”]
    this.textFieldMandator = new TextField(this.i18n.get("login.label.mandator");
    this.textFieldMandator.setWidth("100%");
    this.textFieldMandator.setTabIndex(1);
    this.textFieldMandator.focus();
    this.textFieldMandator.setDebugId("loginMandant");
    [/sourcecode]

Aufzeichnen des Testplans

Wie bereits angedeutet, verwenden wir als Testtreiber das Tool JMeter von Apache. JMeter erlaubt ähnlich wie andere Tools zum Testen von Oberflächen das Aufzeichnen, Anpassen und Abspielen eines Testplans.

Nach dem Start des Tools sieht man zunächst einen leeren Testplan und eine leere Workbench. Um einen Test aufzuzeichnen, fügt man zunächst dem Testplan eine Thread Group hinzu. Über eine Thread Group werden ein oder mehrere Benutzer abgebildet. Der neu hinzugefügten Thread Group fügt man danach noch einen HTTP Cookie Manager hinzu. Dieser sorgt dafür, dass später alle Anfragen des oben beschriebenen Testablaufs auf dem Server einer User Session zugeordnet werden können. Der HTTP Cookie Manager muss so konfiguriert werden, dass die Cookies bei jedem Durchlauf gelöscht werden.

Zusätzlich fügt man der Workbench einen HTTP Proxy Server hinzu. Dieser kann bzw. muss an zwei Stellen konfiguriert werden. Zunächst träg man als Target Controller die zuvor hinzugefügte Thread Group ein. Weiterhin muss evtl. der Port des HTTP Proxy Servers angepasst werden. Der Default Port ist 8080. Dieser Port wird allerdings auch häufig von Servlet Containern wie Tomcat oder in unserem Fall Glassfish verwendet. Wir haben also den Port auf 8090 angepasst.

Apache JMeter
JMeter HTTP Proxy Konfiguration

Nachdem der HTTP Proxy Server konfiguriert wurde, kann dieser nun über den Button Start gestartet werden.

Im nächsten Schritt müssen wir unseren Browser bzw. unser Betriebssystem auf den Proxy konfigurieren. Die Konfiguration hängt stark vom Browser bzw. Betriebssystem ab. Wenn der Browser auf dem gleichen Rechner wie der JMeter Proxy läuft, ist als Host localhost und (in unserem Beispiel) als Port 8090 anzugeben.

Wenn nun noch die Web-Anwendung auf dem Glassfish deployt und dieser gestartet ist, können wir wie gewohnt über den Browser auf die Anwendung zugreifen. Allerdings protokolliert nun der JMeter Proxy die Kommunikation zwischen Browser und Server mit und schreibt diese in die Thread Group des Testplans.

Nachdem wir den oben beschrieben Testablauf manuell einmal durchgeführt haben, können wir den Proxy wieder stoppen, aus der Konfiguration nehmen, und den Testplan speichern.

Anpassen des Testplans

Der Testplan ist ein guter und schneller Anfang, muss allerdings in Bezug auf folgende Punkte nachgearbeitet werden:

  • Der Proxy zeichnet zwar die Aktionen (Requests und Responses) auf. Um eine zuverlässige Aussage über den Erfolg eines Requests zu bekommen (insbesondere bei Lasttests), müssen Requests mit Validierungen (Assertions) versehen werden. Die Responses aus der Aufzeichnung des Testplans können nach manueller Überprüfung der Richtigkeit als Grundlage für die Formulierung einer Assertion herangezogen werden.
  • Der Proxy zeichnet keine Variabilität auf – ergo gilt es, über Zufallsgeneratoren und Variablen die Aufrufe zu parameterisieren.
  • Der Proxy benennt alle Aufrufe des Vaadin-Servlet gleich. Damit ist die Auswertung in JMeter falsch, da nach Namen zusammengefasst wird. Es gilt also, bei Aufrufen an das Vaadin-Servlet mindestens eine Nummer anzuhängen.

Bei der Aufzeichnung des Testplans haben wir gegen einen Glassfish gearbeitet, der auf dem gleichen Rechner wie der JMeter HTTP Proxy Server lief. Daher finden wir in jedem HTTP Request unseres Testplans im Feld Server Name or IP den Wert localhost vor. Diesen Wert müssen wir anpassen, um den Testplan gegen einen Glassfish in der Amazon EC2 laufen zu lassen. An dieser Stelle empfiehlt es sich, eine Variable zu verwenden. Dazu wird der Thread Group eine User Defined Variable hinzugefügt. Die Variable kann man dann über die Syntax ${NAME} in einem HTTP Request referenzieren. Ein Testplan wird übrigens im XML Format gespeichert. Mit einem Texteditor kann man somit schnell in allen HTTP Requests das localhost durch die neu definierte Variable ersetzen.

Benutzer-definierte Variablen anlegen
Benutzer-definierte Variablen anlegen
Benutzer-definierte Variablen verwenden
Benutzer-definierte Variablen verwenden

Die Anzahl der Benutzer, die gleichzeitig auf die Anwendung zugreifen sollen, wird über die Thread Group definiert. Dies geschieht über die Eigenschaft Number of Threads (users) der Thread Group. Neben der Anzahl paralleler Benutzer kann man in der Thread Group auch die Anzahl der Durchläufe festlegen. Entsprechend oft wird dann ein Thread/Benutzer die HTTP Requests der Thread Group ausführen.

Anzahl Threads und Durchläufe
Anzahl Threads und Durchläufe

Damit nicht alle Thread zeitgleich starten (und damit unrealistischerweise exakt zeitgleich auf die Anwendung zugreifen), kann man der Thread Group noch einen Uniform Random Timer hinzufügen.

Wie bereits erwähnt, haben wir für den Test einen Satz von Benutzern angelegt. Alle Benutzernamen beginnen mit test gefolgt von einer aufsteigenden Nummerierung. Damit der Testplan nun diesen Satz von Benutzern verwenden kann, fügen wir unserer Thread Group einen Counter mit dem Namen DURCHGANG hinzu. Dieser Counter kann genau wie eine Variable über dessen Namen in HTTP Request referenziert werden. Den Counter konfigurieren wir außerdem so, dass dieser den Nummernkreis unserer Testbenutzer abdeckt. Wir geben also einen Startwert, ein Inkrement und den Maximalwert an. Ist der Maximalwert erreicht, wird wieder beim Startwert begonnen. Danach muss noch der HTTP Request, der den Inhalt der Login Oberfläche an den Server überträgt, angepasst werden. Im Parameter des HTTP Request findet sich der Benutzername wieder, welchen wir bei der Aufzeichnung des Testplans verwendet haben. Dieser ersetzen wir durch test${DURCHGANG}.

Das Suchen nach einer Eingangsrechnung wollen wir mehrmals pro Login ausführen. Dazu fügen wir der Thread Group einen Loop Controller hinzu und konfigurieren die Anzahl der Durchläufe entsprechend unserer Wünsche. Den HTTP Request, der den Suchbegriff an den Server überträgt, ziehen wir nun auf den Loop Controller. Dieser Request wird damit entsprechend unserer Einstellung x-mal ausgeführt. Der Loop Controller muss dann noch an die korrekte Stelle in der Thread Group gezogen werden (wo vorher der entsprechende HTTP Request war).

Response Assertions
Verwendung eines Loop Controllers

Um zu überprüfen, ob der Server auf einen HTTP Request den korrekten Response gesendet hat, fügen wir den relevanten HTTP Requests jeweils eine Response Assertion hinzu. In unserem Fall wollen wir prüfen, ob das Login geklappt hat, wir erfolgreich in die Eingangsrechnungen wechseln konnten und ob nach Eingabe des Suchbegriffs die korrekte Rechnung angezeigt wird. In einer Response Assertion lassen sich auf verschiedene Eigenschaften des Responses eine Vielzahl verschiedener Prüfungen ausführen.

Response Assertions
Response Assertions

Als letztes sollte man der Thread Group noch einen Summary Report hinzufügen. Dieser liefert uns nach der Testausführung wichtigen Daten für die Bewertung unseres Testergebnisses. Es ist zu beachten, dass Summary Report die Ergebnisse mehrerer Testläufe zusammenfasst. Erst ein Clear oder Clear All in JMeter setzt die Ergebnisse zurück.

Ausführung des Testplans

Eine Amazon EC2 Instanz stellt einen SSH Zugang zur Verfügung. Wenn wir unseren Testplan über SSH auf der Instanz ausführen wollen, steht uns keine grafische Oberfläche zur Verfügung. JMeter lässt sich für diesen Fall im Command Line Mode starten:

[sourcecode]
jmeter –n –t [NAME DES TESTPLANS] –l [NAME der Ergebnisdatei]
[/sourcecode]
Die Option –n startet JMeter ohne grafische Oberfläche. Über –t gibt man den Namen des gespeicherten Testplans an (*.jmx). Mit der Option –l gibt man eine Datei an, in der die Ergebnisse gespeichert werden sollen.

Bevor man den Test startet, muss im Testplan noch der korrekte Host (auf dem die Web-Anwendung erreichbar ist) eingetragen werden. In der Amazon EC2 verändert sich leider mit jedem Start einer Instanz die URL, über den die Instanz erreichbar ist. Entsprechend gilt es auch, fortlaufend nachzutragen.

Nachdem der Test ausgeführt wurde, können wir nun die erzeugte Datei mit den Testergebnissen auf unseren lokalen Rechner kopieren. Um die Ergebnisse zu betrachten, starten wir JMeter wie gewohnt mit der grafischen Oberfläche. Dann legen wir unterhalb der Workbench einen Summary Report an und laden die auf dem Test-Treiber-Server erzeuge Datei in diesen hinein. Die Ergebnisse werden dann zusammengefasst in einer Tabelle dargestellt.

Monitoring der Anwendung während des Tests

Viele interessante Ergebnisse der Ausführung eines Testplans liefert uns bereits JMeter. Wenn wir allerdings den Zustand und das Verhalten unserer Anwendung genauer analysieren möchten, benötigen wir einen tiefen Einblick in die JVM. Dazu dient das Tool VisualVM, das im Lieferumfang des JDKs enthalten ist. Das Tool hat eine grafische Oberfläche und kann somit nicht direkt auf der Amazon EC2 Instanz ausgeführt werden. Stattdessen starten wir es lokal. Damit wir ein Monitoring einer entfernten JVM durchführen können, müssen wir diese JVM erreichbar machen. Dazu dient wiederum das Tool jstatd, das ebensfalls Teil des JDKs ist. Das Tool kann auf der Amazon EC2 Instanz wie folgt gestartet werden:

[sourcecode]
jstatd -p 9999 -J-Djava.security.policy=allpolic
–J-Djava.rmi.server.hostname=ec2-hostname
[/sourcecode]

Die allpolic ist eine Datei mit folgendem Inhalt:
[sourcecode]
grant codebase "file:${java.home}/../lib/tools.jar" {

permission java.security.AllPermission;

};
[/sourcecode]

Damit der Port 9999 von unserem lokalen Rechner erreichbar ist, muss dieser in die Security Group der Amazon EC2 Instanz eingetragen werden.

Leider kommuniziert VisualVM und jstatd über RMI. Nach dem Start von jstatd wird ein weiterer zufälliger Port belegt, der ebenfalls in der Security Group freigeschaltet werden muss. Diesen Port kann man nach dem Start von jstatd über folgenden Befehl ermitteln:

[sourcecode]
netstat -anp | grep <PID des jstatd>
[/sourcecode]

Wenn alles konfiguriert und gestartet ist, fügt man nun in VisualVM unter dem Knoten Remote den Remote Host hinzu. Dieser taucht dann im Baum auf. Unterhalb des Hosts sollten dann alle gestarteten JVMs auf der EC2-Instanz aufgelistet sein.

Um noch detailiertere Auswertungen auf der Remote-JVM zu fahren kann VisualVM statt über jstatd auch über JMX mit der Remote-VM verbunden werden. Das Freischalten von Ports gilt hier analog.

Fazit und Ausblick

Ein Performancetest für eine Vaadin Anwendung mit Hilfe von JMeter und der Amazon EC2 ist kein Hexenwerk. Es gilt zweifellos, Klippen zu umschiffen, allerdings entsteht, wie wir oben gezeigt haben, schnell und ohne große eigene Investition eine Umgebung, die verlässliche Aussagen ermöglicht. Auch die gute Testbarkeit von Vaadin trägt dazu bei, ein realistisches und aussagekräftiges Szenario aufzubauen und durchzutesten.

Im zweiten Teil werden wir auf die Testergebnisse unserer Läufe und die daraus resultierenden Erkenntnisse eingehen.

Wenn Sie Fragen haben, schreiben Sie einfach einen Kommentar oder schreiben Sie uns eine E-Mail:

oliver.damm [at] akquinet.de
sebastian.rothbucher [at] akquinet.de