Warum Lasttests auch für Entwickler interessant sind

Keine Kommentare

Lasttests mit Hilfe von Tools wie JMeter oder Gatling sind ein gängiges Mittel um die Performance von Anwendungen zu analysieren und zu verbessern. In vielen Fällen stehen diese Tests am Ende der Entwicklung. Manchmal kommen sie erst dann zum Einsatz, wenn bereits erste Performance-Probleme im Live-Betrieb aufgetreten sind. Auch Monitoring und Fehlertoleranz sind Themen, die in der laufenden Entwicklung oft zu kurz kommen.

Viele Probleme ließen sich vermeiden, wenn bereits während der Entwicklung mehr Wert darauf gelegt wird, das Verhalten der Anwendung unter Last zu untersuchen. Das bedeutet auch, dass Lasttests nicht mehr nur in dedizierten, produktionsähnlichen Umgebungen ausgeführt werden, sondern auch in der lokalen Entwicklungsumgebung des einzelnen Entwicklers. Lasttests (und die zugehörigen Tools) sollten zum Handwerkszeug eines jeden Entwicklers gehören – genauso wie Unit-, Integrations- und Akzeptanztests.

Das hört sich komisch an, schließlich hat ein Lasttest auf einem kleinen Entwickler-Notebook wenig Aussagekraft. Es gibt aber eine ganze Reihe von Szenarien, in denen es gar nicht darum geht, die Leistungsfähigkeit des Systems unter realistischen Bedingungen zu analysieren. Oft geht es viel mehr darum, zu verstehen, wie sich einzelne Komponenten verhalten, wenn das ganze System unter Last gesetzt wird.

Im folgenden Beispiel wird mit Hilfe eines lokalen Lasttests, ein konkretes Problem untersucht: Wie reagiert meine Anwendung, wenn die Datenbank plötzlich nicht mehr erreichbar ist? Man kann natürlich argumentieren, dass die Konfiguration der Datenbankverbindung klar in die Verantwortlichkeit des Administrators fällt. Es gibt aber einige triftige Gründe, warum auch ein Entwickler verstehen muss, wie eine Datenbankverbindung sinnvoll konfiguriert wird und was bei fehlerhafter Konfiguration passieren kann:

  • Der Administrator kann nur das konfigurieren, was von außen konfigurierbar ist. Wenn aber der Datenbankverbindungspool innerhalb der Applikation (z.B. im Spring-Applikationskontext) definiert wird, ist es die Aufgabe des Entwicklers, die nötigen Optionen konfigurierbar zu machen.
  • Um herauszufinden, wie eigentlich „sinnvolle“ Einstellungen aussehen, ist der Administrator auf Rückmeldung von der Applikation angewiesen. Konkret heißt das: Log-Meldungen bei (möglichen) Problemen und Monitoring für wichtige Ressourcen (z.B. JMX-Anbindung für den Datenbankverbindungspool) müssen vom Entwickler bereitgestellt werden.
  • Angenommen der Administrator findet sinnvolle Werte für Timeouts. Damit ist die Sache nicht erledigt. Die Applikation muss mit Timeouts auch umgehen können. Exceptions müssen sinnvoll behandelt werden (Stichwort Logging und Exception-Mapping). Insbesondere im Fall von Timeouts sind die Exceptions zum Teil mehrfach geschachtelt und es ist gar nicht so einfach, an die nötigen Informationen zu kommen. Hier hilft nur eins: Selbst Hand anlegen, Lasttests durchführen, Fehler provozieren und Stacktraces studieren.

Beispielszenario – Datenbankausfall

An dieser Stelle könnte man sich auch ein ausgefeiltes Beispiel einer fehlertoleranten, verteilten Cloud-Anwendung überlegen. Doch so kompliziert muss es überhaupt nicht sein. Die meisten Anwendungen basieren auf einer Drei-Schichten-Architektur und sind damit sowieso schon „verteilt“, d.h. die Persistenzschicht (Datenbank) läuft unabhäng von der Anwendungslogik. Was passiert also, wenn in einer typischen Java Enterprise Webanwendung plötzlich die Datenbank ausfällt?

Klar ist zumindest, was passieren sollte: Die Webanwendung sollte in irgendeiner Weise dem Anwender mitteilen, dass im Moment keine Anfragen bearbeitet werden können (z.B. durch einen HTTP 5xx Fehler). Sobald die Datenbank wieder verfügbar ist, sollte die Anwendung wieder ganz normal funktionieren.

Test-Setup

Die verwendete Beispielanwendung ist so einfach wie möglich gehalten und basiert im wesentlichen auf der Spring Pet Clinic. Die Eckdaten der Webanwendung:

  • Spring und Spring-MVC
  • Tomcat 7 in Standardkonfiguration
  • Datenbank MySQL (Version: 5.5.22)
  • MySQL per JDBC angebunden (Version des Treibers: 5.1.22)
  • Connection-Pool DBCP in Standardkonfiguration

Der Lasttest wurde mit JMeter durchgeführt. Im konkreten Test geht es nicht darum, so viel Last wie möglich zu erzeugen. Vielmehr sollen möglichst gleichmäßig HTTP-Requests geschickt werden, damit später der Unterschied in den Antwortzeiten deutlich wird. Gemessen wird nur die Antwortzeit, unabhängig davon, ob der Request erfolgreich war oder nicht. Während der Test läuft, wird die Datenbankverbindung getrennt und anschließend wiederhergestellt. Die Eckdaten des JMeter-Tests:

  • 20 User Threads
  • Jeder Thread setzt pro Iteration einen HTTP GET-Request ab
  • Timeout für HTTP-Requests: 60 Sekunden
  • Wartezeit zwischen einzelnen Requests: eine Sekunde

Erster Testdurchlauf – Standardkonfiguration

JMeter Test - Antwortzeiten - ohne Timeouts

Solange die DB erreichbar ist, liegen die Antwortzeiten bei ca. 300 ms. Nachdem die Verbindung zur DB getrennt wurde, schnellen die Antwortzeiten auf über eine Minute hoch. Dass der Graph überhaupt noch lesbar ist (und die Spitze nicht in den Wolken verschwindet), liegt daran, dass im JMeter Test ein Client-Timeout von 60 Sekunden gesetzt ist. Tatsächlich antwortet die Anwendung überhaupt nicht mehr!

Um zu erfahren, warum das so ist, hat man als Entwickler viele Möglichkeiten: Von Basic-Tools wie Java VisualVM über Profiler bis hin zum Debugger hat man Zugriff auf alles, was einem das Java-Ökosystem zur Verfügung stellt. Ein Administrator hat an dieser Stelle weit weniger Optionen. Meistens bleibt nur der Blick ins Logfile (und dort herrscht in diesem Fall gähnende Leere). Dieser Problematik sollte man sich als Entwickler bewusst sein. Deshalb ist es wichtig, Mechanismen einzubauen um bei möglichen Problemen entsprechendes Feedback zu liefern. Eine Anwendung die sich einfach nur tot stellt, ist sehr schwer zu administrieren.

Im konkreten Fall hilft es, sich einen Thread Dump zu holen (z.B. mit VisualVM). Dort wird man feststellen, dass alle Threads warten. Manche warten auf Antwort von der Datenbank. Andere warten darauf, dass sie vom Connection-Pool eine DB-Verbindung zugewiesen bekommen. Und alle warten ohne Timeout, also potentiell ewig!

Zweiter Testdurchlauf – Angepasste Konfiguration

Sehen wir uns an, wie es besser funktionieren kann. Im folgenden Test wurde der ConnnectTimeout des MySQL-Treibers auf 3 Sekunden und der SocketTimeout auf 10 Sekunden gesetzt. Im Graphen kann man gut erkennen, wie nach dem Datenbankausfall zuerst der Socket-Timeout für bestehende Verbindungen greift – die Antwortzeit liegt kurzzeitig bei ca. 10 Sekunden. Anschließend wird bei jedem Request versucht eine neue Verbindung zur Datenbank aufzubauen. Diese Anfragen werden nach jeweils 3 Sekunden mit einer entsprechenden Fehlermeldung quittiert. In jedem Fall antwortet die Anwendung weiterhin. Auch im Logfile tauchen jetzt entsprechende Meldungen (inklusive Stacktraces) auf und weisen auf die kaputte Datenbankverbindung hin.

JMeter Test - Antwortzeiten - mit Timeouts

Der nächste Schritt wäre nun, sich Konfigurationsparameter des Verbindungspools anzusehen (in diesem Fall DBCP). Man kann auch andere Verbindungspools ausprobieren. Die JMX-Anbindung der verschiedenen Pools sind ebenfalls einen Blick wert. Durch dieses „Herumprobieren“ erfährt man nicht nur einiges über die verwendeten Technologien. Jede Anpassung bringt auch Vorteile hinsichtlich Performance, Fehlertoleranz und Monitoring. Alles unterstützt durch einen sehr simplen Lasttest.

Fazit

JMeter und Konsorten sind nützliche Tools, die leider viel zu selten während der Entwicklung eingesetzt werden. Ihr Einsatz bietet den Entwicklern tieferes Verständnis in die Internas der Systeme und rückt oft vernachlässigte Themen in den Vordergrund. Daneben gibt es auch eine Reihe sehr konkreter Anwendungsfälle, in denen Lasttests eine große Hilfe darstellen:

  • Unterstützung bei der Auswahl von Bibliotheken (z.B. Connection-Pool, Cache, …)
  • Finden von Schwachstellen im Monitoring und Logging
  • Testen von Mechanismen für Fehlertoleranz (z.B. Circuit Breaker, Fail Fast Guard, …)
  • Identifizieren möglicher Performance-Probleme (z.B. durch Analyse der Zugriffe auf Fremdsysteme)

Ein Prinzip der agilen Softwareentwicklung ist, Probleme durch kontinuierliches Testen so früh wie möglich zu identifizieren und zu beheben. Das gilt für Monitoring, Fehlertoleranz und Performance ebenso wie für funktionale Anforderungen. Deshalb sollten Lasttests zum Repertoire eines jeden guten Entwicklers gehören.

Michael Lex ist agiler Softwareentwickler bei codecentric. Neben der täglichen Arbeit mit Java EE oder Spring gilt sein besonderes Interesse dem weiten Feld der Datenanalyse, insbesondere Machine-Learning-Algorithmen.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.