Praxis 17.08.2015, 08:42 Uhr

Last- und Performancetests ? (k)eine besondere Kunst?

Performance ist mit die wichtigste nichtfunktionale Anforderung, die an jede Anwendung gestellt wird, besonders jedoch an serverbasierte Anwendungen.
Artikel direkt im Zhlke-Blog lesen Jeder Entwickler kennt Horrorgeschichten von Projekten, die nach monate- oder gar jahrelanger Entwicklung scheitern, weil sie zwar die funktionalen Anforderungen erfüllen, aber schon mit wenigen Usern überlastet sind und die Performanceprobleme auf grundlegende Architekturentscheidungen zurückgehen. Das beste Mittel, um solche Desaster zuverlässig zu vermeiden, besteht darin, die Peformance genauso zu testen wie die Funktionalität und zwar frühzeitig und systematisch. Dabei sind jedoch viele Faktoren zu berücksichtigen, um aussagekräftige Ergebnisse zu erzielen. Es gibt kein “zu früh” Ein häufiger Fehler ist, Lasttests erst gegen Ende eines Entwicklungsprojekts einzuplanen, wenn das öffentliche Release schon kurz bevorsteht. Dies kann fatal sein. Denn während grundlegende, weitreichende funktionale Probleme während der Entwicklung fast zwangsläufig auffallen, kann eine völlig unzureichende Performance unbemerkt bleiben, da ja nie eine nennenswerte Last auf das System kommt. Und wenn dann architekturelle Fehlentscheidungen die Ursache sind, sind diese wahrscheinlich nicht mehr im verfügbaren Zeit- und Budgetrahmen behebbar. Daher sind Lasttests möglicht frühzeitig durchzuführen, im Rahmen einer inkrementellen Entwicklung entweder an einzelnen Komponenten oder bereits fertiggestellter Durchstichfunktionalität. Zudem sollten sie kontinuierlich laufen, um Fehlentwicklungen schnell zu erkennnen und korrigieren zu können, bevor weitere Abhängigkeiten entstehen. Ideal sind also automatisierte Tests, die täglich durchgeführt werden – in Zeiten des Cloud Computing sind die Hardwareressourcen dafür ja im Prinzip für jeden verfügbar. Performancekriterien Für ein konkretes Testergebnis muss man Performance messbar, quantifizierbar machen. Die wichtigsten Leistungsdaten sind dabei: Antwortzeit – Die Zeit, die das System benötigt, um auf eine Anfrage eine Antwort zu liefern, typischerweise in Millisekunden. Teilweise kann es sinnvoll sein, dabei zu unterscheinden zwischen der Zeit bis die Antwort beginnt einzutreffen und bis sie vollständig ist.Durchsatz – Die Zahl der Anfragen, die das System insgesamt bearbeiten kann, typischerweise in Anfragen/Transaktionen pro Sekunde (TPS)Die beiden Größen hängen natürlich zusammen; normalerweise steigt die Antwortzeit unter zunehmender Last moderat an, bis irgendwann die Anfragen schneller kommen als das System sie bearbeiten kann und die Antwortzeiten explodieren. Es ist also immer notwendig beide Kriterien zusammen zu messen. Wichtig ist auch die Frage, wie man aus einer Vielzahl von Messereignissen ein Gesamtergebnis ableitet: Jede Anfrage die bearbeitet wurde, ist ein Messpunkt mit Antwortzeit und Zeitpunkt. Für den Durchsatz berechnet man die Anzahl der Ereignisse pro Zeiteinheit. Hier ist im Wesentlichen nur die Granularität wichtig, um einerseits zufällige Häufungen auszublenden, andererseits aber Veränderungen im Lauf eines Tests sichtbar zu machen.Die Antwortzeit ist komplizierter. Weder Maximum noch Minimum sind besonders aussagekräftig, und auch ein einfacher Durchschnittswert ist nicht ideal, da sich hinter einem “ordentlichen” Wert eine sehr uneinheitliche Performance verbergen kann. Eine verbreitete Alternative ist der Perzentilwert, eine Erweiterung des Mediankonzepts. Die Antwortzeit des 95. Perzentils ist die Zeit, bei der gerade 95% der Antworten schneller erfolgten. Egal welche Größen man wählt, sie sollten wie der Durchsatz im zeitlichen Verlauf betrachtet werden.Testziel Einfach mal einen Lasttest zu machen mag den einen oder anderen Engpass aufdecken. Aber letztendlich ist es Zeitverschwendung, wenn man sich nicht überlegt hat, welches konkrete Ergebnis man haben will. Daher sollte man sich vor dem Aufsetzen der Tests schon Gedanken zum Testziel machen. Am klarsten ist eine direkte Vorgabe eines zu erreichenden Performanceziels auf Basis einer Lastprognose, nach dem Muster: Ob das Ziel erreicht wurde, ist also eine einfache Ja/Nein-Frage, was den Vorteil hat, dass man bei einem “Nein” eine klare Handlungsaufforderung hat: Es muss etwas getan werden, um das Ziel wieder zu erreichen. Alternativ kann aber auch eine “offene Frage” gestellt werden, um eine Kenngröße als Messergebnis zu erhalten: Hierzu sind Testserien oder eine sehr langsame Steigerung des Durchsatzes innerhalb eines Testlaufs erforderlich, in jedem Fall steigt der Zeitaufwand. Mehr als eine Kenngröße sollte daher nicht offen sein. Testumgebung, Testdaten, Testplan Für Lasttests gilt: “je realistischer, desto besser”, d.h. der Test sollte die reale Lastsituation so genau wie möglich nachstellen. Im Einzelnen: Die insgesamt simulierte Last sollte der tatsächlich erwarteten entsprechen. Wenn real 500 Requests/s von 5 Servern bewältigt werden sollen, ist man versucht, stattdessen 100 Requests/s auf nur einem Server zu testen und das Ergebnis zu extrapolieren. Dies ist jedoch riskant, da so leicht Engpässe an unerwarteten Stellen unentdeckt bleiben, z.B. eine Firewall die nur 200 Requests/s verkraftet.Es sollte also die Hardware der Testumgebung zu der geplanten Produktionsumgebung identisch sein. Dies betrifft nicht nur die Server selbst, sondern auch die Infrastruktur, wie Loadbalancer und Firewalls. Dadurch erreicht die Testumgebung auch beträchtliche Komplexität und erfordert DevOps-Kompetenz, um sie zu managen.Auch die Softwareausstattung muss identisch sein: Betriebssystem, Serverkomponenten und natürlich das zu testende System.Die Datenbasis muss ebenfalls realistisch sein, d.h. die Menge und Struktur der bestehenden und durch den Test erzeugten bzw. verwendenten Daten. Denn die Performance von Datenbanken sieht oft ganz anders aus, wenn erst einmal 5 Millionen Benutzeraccounts angelegt sind. Dieser Aspekt bereitet oft unerwartet Schwierigkeiten, da echte Produktionsdaten noch nicht existieren oder aus Datenschutzgründen nicht verwendet werden dürfen – die Testdaten müssen also künstlich generiert oder anonymisiert werden.Die Verwendung der Datenbasis (insbesondere von Benutzeraccounts) durch die Testtreiberkomponenten muss ebenfalls dem Verhalten der erwarteten echten Usern entsprechen. Es müssen also genug unterschiedliche Domänenobjekte nicht nur angelegt, sondern auch parallel verwendet werden. Andernfalls könnte die gemessene Performance unrealistisch gut (z.B. durch Caching) oder auch schlecht (z.B. durch lock contention) ausfallen.Schließlich muss ein Lastprofil definiert werden, d.h. welche Services wie oft aufgerufen werden. Quelle hierfür können Logs aus dem Produktivbetrieb sein oder Hochrechnungen aus Use Cases und Business Cases.Dazu gehört auch die Frage, wie lange die Last gefahren werden soll und wie sie sich zeitlich ändert. Meist ist mindestens ein Rampup sinnvoll, d.h. ein graduelles Hochfahren der Last zu Beginn, da viele Komponenten eine (auch in der Realität eigentlich immer vorhandene) zeitlich leicht gestreute Last viel besser verkraften als eine unvermittelt einsetzende Vollast.Generell wird man hier eine Kosten-Nutzen-Abwägung machen. Auch ein teilweise unrealistischer Test kann schon viele Probleme aufdecken. Wenn aber der Relaunch eines großen Webshops mit einer millionenschweren Marketingkampagne ansteht, dann spart man sicher am falschen Ende, wenn man davor zurückschreckt, auch eine sechsstellige Summe für eine dedizierte Lasttestumgebung auszugeben. Monitoring Ein Defekt, der sich nicht zuverlässig oder nur nach längerer Laufzeit reproduzieren läßt, ist schwer zu beheben. Und bei Performanceproblemen ist dies nicht eine unangenehme Ausnahme, sondern der Normalfall. Daher kann man sich bei Performancetests nicht auf eine Live-Beobachtung des Systems verlassen, sondern muss die Voraussetzungen für eine Post-hoc-Analyse schaffen. Dazu muss zunächst das Testergebnis aufgezeichnet werden, also die Leistungsdaten und ihre Entwicklung über den Testverlauf hinweg. Auch die Systemtätigkeit selbst, also z.B. Serviceanfragen und -antworten sollten mitgeloggt werden, schon um zu erkennen, ob das System überhaupt funktional korrekt arbeitet (und man nicht nur misst, wie schnell es Fehlermeldungen ausgeben kann). Ein oft unterschätzter Aspekt ist das Monitoring des Systems, denn der Test soll ja nicht nur herausfinden, dass das System die Performanceziele ggf. nicht erreicht, sondern vor allem warum. Dazu ist es wichtig den Systemzustand möglichst auf allen Ebenen zu beobachten und aufzuzeichnen: Anwendungs- und SystemlogsIO-Auslastung (Netzwerk, Festplatten)CPU-Auslastung / LoadfaktorSpeicherauslastung (RAM, VM-Heap)Datenbank-Kenngrößen (Connection pools, sessions, Undo-Logs)GC-AktivitätKenngrößen der Anwendung, z.B. Queues und Puffer, interne LaufzeitenDie Testtreiber müssen ebenfalls überwacht werden, da auch hier Engpässe auftreten können, die das Ergebnis verzerren.Auswertung Bei der Auswertung des Testergebnisses überprüft man zunächst, ob fachliche oder technische Fehler aufgetreten sind, die durch fehlerhafte Testfälle oder Überlastung entstehen können. Falls nicht, gleicht man die aufgezeichneten Leistungsdaten mit dem Testziel ab. Im Idealfall ist alles OK und das System hat den Test bestanden. Sind Probleme (Fehler oder Nichterfüllung des Testziels) aufgetreten, sucht man in den Logs und Monitoring-Daten nach der Ursache. Hierbei ist oft eine durchaus komplexe root-cause-Analyse notwendig, da eine Überlastung des Systems oft zu einer Fehlerkaskade führt, die den eigentlichen Engpass verschleiert. Um eine Übersicht über die große Menge an Daten aus unterschiedlichen Quellen zu gewinnen, sind graphische Darstellungen – besonders in Form von Dashboards – nützlich. In vielen Fällen wird man aber auch den Test wiederholen, um dabei das Verhalten live zu beobachten und gezielt zusätzliche Daten zu erheben, wie z.B. thread- oder heap-dumps. Auch wenn man sich sicher ist den Engpass bzw. die Ursache der schlechten Performance gefunden zu haben, lohnt es sich, die Analyse noch etwas weiterzutreiben. Gerade bei komplexen Systemen ist schlechte Performance oft auch die Folge von falscher Konfiguration statt von ineffizientem Code. Oft wird man am Ende eher Vermutungen über die eigentliche Ursache des Problems haben als absolute Sicherheit. Nacharbeit Nach der Behebung des Problems ist selbstverständlich ein Retest notwendig, um sicherzustellen, dass das gewünschte Ergebnis auch erreicht wurde und keine problematischen Nebenwirkungen auftreten. Eventuell wird man auch verschiedene Lösungsvarianten ausprobieren. Wichtig ist dabei einen breiteren vorher/nachher-Vergleich anzustellen und nicht nur die Leistungsdaten oder das behobene Problemverhalten anzuschauen, denn Nebenwirkungen können ja auch in anderen Bereichen auftreten (z.B. wenn eine Codeänderung zwar die CPU-Last reduziert, aber die Speicherauslastung in kritische Bereiche bringt). Dazu ist es nötig, die oben genannten Monitoring-Daten über eine gewisse Zeit aufzubewahren. Nach dem Performancetest ist also grundsätzlich vor dem Performancetest, aber der Aufwand lohnt sich: Man will sich schließlich auf den Erfolg seines Systems freuen können und nicht Angst davor haben, dass es der Last nicht gewachsen ist!


Das könnte Sie auch interessieren