Performance-Optimierung bei Java-Webanwendungen – Do's und Don'ts
Woher kommt die Motivation, um sich mit dem Thema Performance-Optimierung auseinanderzusetzen? Da gibt es meist zwei Gründe: Der erste Grund ist, dass es einfach Spaß macht, etwas noch effizienter zu gestalten, um noch ein letztes bisschen Performance aus seinem Quelltext zu kitzeln. Nur leider bekommt man nicht immer die dafür notwendige Zeit zur Verfügung gestellt.
Der zweite Grund gibt einem die Möglichkeit, sich sehr detailliert mit dem Thema auseinanderzusetzen: Die Anwendung ist dem Kunden zu langsam, es wird weniger Geld verdient, da bestimmte Prozesse nicht performant genug abgebildet worden sind. Es gibt viele Untersuchungen und Statistiken, die aufzeigen, wie kostenintensiv schlechte Performance ist. Nun kann man all sein Wissen in die Waagschale werfen.
Aber: wann beginnt das Thema Performance? Wie kann man sich dem Problem nähern? Welches Werkzeug kann helfen?
Was ist Performance?
Die Performance einer Anwendung setzt sich aus vielen verschiedenen Faktoren zusammen. Diese Faktoren sind natürlich unterschiedlich, je nachdem von welcher Art Anwendung die Rede ist. Im Rahmen dieses Artikels legen wir daher den Fokus auf Java-Webanwendungen. Die Hauptperformance-Kriterien sind hier Antwortzeit, Durchsatz (Anfragen pro Zeiteinheit) und Ressourcenauslastung.
Mit Antwortzeit ist die Zeit gemeint, die es dauert, eine HTTP-Anfrage zu beantworten. Wichtig ist an dieser Stelle, dass auch die Antwortzeit aus Endbenutzersicht betrachtet wird. Diese setzt sich zusammen aus dem Netzwerkroundtrip, der Serververarbeitungszeit und der Zeit, die der Browser benötigt, die Seite zu rendern. Viele Monitoring-Tools bieten hier nur rudimentäre Statistiken an, die sich auf Durchschnittswert und Standardabweichung beschränken. Da Antwortzeiten in der Regel aber alles andere als normalverteilt sind, reicht das nicht aus, um die Antwortzeit adäquat bewerten zu können. Besonders Ausreißer können den Durchschnittswert stark beeinflussen, sodass dieser nicht den typischen Wert der Antwortzeiten widerspiegelt. Hier kommen Perzentile ins Spiel. Das 50%-Perzentil, auch Median genannt, teilt die Antwortzeiten in zwei Hälften. Ist der Wert des Median beispielsweise fünf Sekunden, so wurden 50% der Anfragen schneller als fünf Sekunden abgearbeitet und 50% langsamer. Betrachtet man allerdings nur den Median, kennt man nur das typische Antwortzeitverhalten der Anwendung, kann aber noch keine Aussage darüber treffen, ob es evtl. viele Anfragen gibt, welche nur sehr langsam bearbeitet wurden – Stichwort Ausreißer. Hierauf gibt beispielsweise das 95%-Perzentil Aufschluss, welches die Ausführungszeit der fünf Prozent der langsamsten Anfragen wiedergibt.
Wenn einer Anwendung zu wenige Ressourcen zur Verfügung gestellt werden, können keine zufriedenstellenden Antwortzeiten erreicht werden, was wiederum zu Umsatzeinbußen führen könnte. Aber auch eine Überdimensionierung ist zu vermeiden, denn Ressourcen sind nicht kostenlos. Werden sie nicht genutzt, hätte das dafür aufgebrachte Geld in andere Dinge mit einem höheren Return-on-Investment investiert werden können. Wie stark die Ressourcen ausgelastet werden können, hängt stark von den Gegebenheiten ab. Ist die Last sehr konstant, muss eine Vollauslastung der CPU nicht unbedingt negativ sein. Treten jedoch regelmäßig Lastspitzen auf, so sollten die Reserven üppiger gewählt oder eine elastische Skalierung ermöglicht werden.
Bei Java-basierten Anwendungen ist der Heap bzw. der Garbage Collector eine kritische Ressource. Kommt es zu einer Major Collection (Garbage Collection), so werden alle Anwendungsthreads angehalten und es können in dieser Zeit keine Anfragen beantwortet werden. Typischerweise gibt es in einem Application-Server eine Reihe an Ressourcen-Pools – beispielsweise der Thread-Pool des Servers der die Threads zur Abarbeitung der Anfragen zur Verfügung stellt, der JDBC-Connection-Pool oder weitere Thread-Pools der Anwendung für die asynchrone Verarbeitung von Tasks. Wenn die Ressourcen des Pools ausgeschöpft sind, müssen Anfragen so lange warten, bis beispielsweise ein Thread wieder frei wird. Ebenso kritisch ist eine Überdimensionierung der Pools. Kann ein Server beispielsweise nur 100 gleichzeitige Anfragen effizient verarbeiten, man lässt aber 200 zu, wird der Server bei Spitzen im Traffic möglicherweise überlastet, sodass keine einzige zufriedenstellend schnell beantwortet werden kann oder der Server sogar "abstürzt". Des Weiteren sollte man ein Auge auf die Metriken des Betriebssystems haben.
Application Performance Management
Application Performance Management ist einer der Begriffe unter denen sehr viel zusammengefasst werden kann. Wir werden uns nun diesem Begriff nähern und die für uns wichtigen Dinge herausarbeiten.
Gehen wir davon aus, dass Änderungen an bestehenden Anwendungen zumeist teurer sind als in den ersten Entwicklungszyklen. Das bedeutet im Umkehrschluss, dass Änderungen aufgrund von Performanceengpässen am besten zu Beginn vorgenommen werden sollten. Das aber ist kein guter Ausgangspunkt, da wir dazu sehr genau wissen müssten, wie das tatsächliche Lastverhalten sein wird. Wir haben also das typische Huhn-oder-Ei-Problem. Später werde ich noch genauer darauf eingehen, wenn wir uns mit dem Themenbereich Lasttests beschäftigen.
Es gibt viele Untersuchungen die auf die eine oder andere Art belegen, dass Modifikationen zu Beginn der Entwicklungsphase eines Software-produktes/-projektes kostengünstiger sind. Oftmals unterstellt man einen exponentionellen Verlauf der Kostenfunktion. Richtig ist auf jeden Fall, dass die Lebensspanne einer Anwendung klassisch in drei Phasen unterteilt werden kann: Development, QA, Production. An dieser Stelle werden mir die Entwickler aus der agilen Softwareentwicklung widersprechen wollen. Aber selbst einen Sprint kann man gedanklich in diese drei Phasen aufteilen.
Wenn an sich nun die Entwicklung selbst ansieht, so hat man folgende Stufen:
- Anforderung
- Entwicklung
- Qualitätssicherung
- Produktion/ Wartung
Wünsch-Dir-Was-Veranstaltungen liefern häufig keine messbaren Werte.
Wo also beginnt nun Application Performance Management? Die Antwort ist schnell gegeben. Es beginnt schon bei der initialen Projektphase. In den Vertragswerken, die ein Projekt begleiten, werden bzw. sollten SLAs definiert werden. Hierbei ist zu beachten, dass dieses natürlich im Idealfall auf den jeweiligen Use Case heruntergebrochen wird. Dieses kann iterativ geschehen, sollte aber schon zu Beginn die massgeblichen Kennzahlen liefern. Darunter verstehen wir z. B. die Anzahl der zu erwartenden parallel aktiven Benutzer, genauso wie die Definition der maximalen Antwortzeit die einem Use Case als Reaktionsgeschwindigkeit zu Verfügung steht. Und hier beginnt auch schon die Herausforderung. Oftmals haben die ersten Definitionen eher den Charakter einer "Wünsch-Dir-Was"-Veranstaltung. Hier gilt es die Definitionen mit dem Fachbereich und den Architekten gemeinsam zu gestalten und soweit zu finalisieren, dass diese Werte schon vor der Entwicklung zur Verfügung stehen. Es ist nicht immer leicht, in diesen Diskussionen zu messbaren Werten zu kommen. Warum dem so ist? Angaben wie "angemessen schnell" kann man recht zügig aus den Definitionen streichen, da jedem klar wird, dass es schwer zu messen sein wird. Allerdings wird der Punkt ein wenig komplizierter, wenn die Angaben zu der Performance Implikationen auf die zu erwartenden Kosten hat. Hier gilt es, schnell die für beide Seiten vertretbaren Größenordnungen zu finden.
Sollte es sich z. B. um B2C-Web-Anwendungen handeln, kann man auch Vergleichswerte von Mitbewerbern oder ähnlichen Anwendungen holen und diese als Basis nehmen. Hierzu gibt es einige Untersuchungen, wie sich Abweichungen von diesen Werten in dem zu erwartenden Benutzerverhalten/Umsatzvolumen niederschlägt. Auch hier gilt es, schon zu Beginn ein Gefühl für die tatsächlichen Hebel in der Anwendung zu finden um die zur Verfügung stehenden Ressourcen gezielt einsetzen zu können.
Bei der Definition der Grenzwerte ist auch zu beachten, wie in den Randbereichen reagiert werden soll. Darf eine Zeitlinie überschritten werden und wenn ja, wie oft und wie lange? Ein Bremssystem darf sicherlich nicht das selbe Lastverhalten an den Tag legen wie die Übersicht über das Abendprogramm eines Kinos.
Ebenfalls ist sicherzustellen, dass die Werte, die für die Definition der SLAs verwendet werden, auch zuverlässig ermittelt werden können. Manchmal ist das eine nicht zu unterschätzende Anforderung.
Nachfolgend ein Beispiel für eine mögliche Definition, die zur Bewertung eines Use Cases verwendet werden könnte:
Die Suche sollte in 95% der Fälle eine Reaktionsgeschwindigkeit von unter 0,7 Sekunden haben. In max 1% der Fälle darf die Reaktion länger als 2 Sekunden dauern.
Ebenfalls sollte eine Definition der zu erwartenden Benutzer erfolgen. Dabei spielt die Nutzeranzahl und die Anzahl parallel agierender Benutzer eine Rolle. Gibt es Abhängigkeiten zu Datum und Zeit? Oftmals werden die zu erwartenden Benutzer über einen 24h-Tag gleichverteilt. Ein denkbar schlechter Ansatz, wenn es sich um eine betriebliche Anwendung handelt, die nur zu den normalen Bürozeiten verwendet wird. Aber auch die Verteilung auf die Use Cases sollte vorgenommen werden. Nicht alle Benutzer werden gleichverteilt alle zur Verfügung stehenden Use Cases verwenden. Und ebenfalls nicht unwichtig ist die Betrachtung, welcher dieser Use Cases Auswirkungen auf den Ertrag hat. Schön ist es, wenn die Auswahl der Hintergrundfarbe stabil unter Last funktioniert. Ärgerlich, wenn das für den Bezahlvorgang dann nicht mehr zutrifft. Alle diese Definitionen sind die Grundlage für die ersten Lasttests. Leider wird zu oft vergessen, nach der Definition der Grenzwerte auch über die zur Verfügung stehenden Ressourcen zur Lasterzeugung nachzudenken. Hier kann es zu beträchtlichen Kosten und Aufwänden kommen.
Tuning
Das größte Potential zur Performancesteigerung liegt meistens in der Gestaltung des Anwendungscodes bzw. der Businesslogik. Diesen Bereich sehen wir uns daher im Folgenden näher an. Weitere wichtige Themen und Bereiche der Performanceoptimierung sind Browser Tuning (JavaScript-Ausführung, Komprimierung und Konkatinierung von Ressourcen, ...), Garbage Collection/Memory Tuning, Application Server Tuning sowie Tuning von Caches und Ressourcen-Pools.
Zu allererst muss der richtige Einstiegspunkt identifiziert werden. Hier können im Allgemeinen bei Webanwendungen folgende Erkennungsmerkmale angewendet werden:
- Permanent zeitintensive Use Cases
- Volatile Use Cases – also solche, die hin und wieder sehr zeitintensiv sind
- Use Cases, die in Summe viel Zeit in Anspruch nehmen
Wurden für das betreffende Projekt SLA's definiert, kann das als Entscheidungsgrundlage verwendet werden. Ebenfalls sollte immer mit in die Betrachtung einfließen, wie geschäftskritisch der jeweils betrachtete Bereich ist.
Permanent zeitintensive Use Cases
Um permanent langsame Use Cases zu erkennen, können die Ausführungszeiten nach dem Median sortiert aufgelistet werden. Hierdurch erhält man eine Liste der langsamsten Anwendungsfälle. Die Voraussetzung hierfür ist natürlich, dass man Antwortzeitstatistiken incl. Perzentile gruppiert nach Use Case zur Hand hat.
Ist nun der betreffende Use Case identifiziert, so werden detailiertere Informationen benötigt. In diesem Fall der Call Tree. Ein Call Tree liefert die Informationen aus welchen Sub-Requests bzw. Methodenaufrufen sich der betrachtete Use Case zusammensetzt und wieviel der verwendeten Zeit sich wohin zuordnen lässt. Das führt dazu, dass die wesentlichen Teile nun in die nähere Analyse genommen werden.
Häufige Ursache langsamer Requests sind exzessive Netzwerkzugriffe. Das ist oft der Fall, wenn Daten feingranular aus dem relationalen Persistencelayer geladen werden. Hier kann man mit verschiedenen Techniken zum Ziel gelangen. Diese reichen von der Auswahl eines dem Problem adäquaten Persistencelayers bis hin zur Modifikation des Datenmodells und die darauf aufbauenden Zugriffe.
Welche Optimierungen vorgenommen werden können ist natürlich immer Applikationsabhängig. Meist geht es aber nicht darum, mittels "magischer Hacks" oder aus dem Kontext gelösten Annahmen wie beispielsweise "statische Methodenaufrufe sind doch schneller – Ich verwende nur noch statische Methoden", einen Geschindigkeitsgewinn zu erzielen. Vielmehr von Bedeutung ist es, genauer zu verstehen, welche Daten notwendig sind und diese effizient zu laden.
Zunächst sollte man sich einen Überblick verschaffen, welche Daten tatsächlich benötigt und auf der Webseite angezeigt werden. Es macht zumeist keinen Sinn, 100 Suchergebnisse anzuzeigen, wenn nur vier ohne zu scrollen auf einer Bildschirmseite sichtbar sind. Eine weit verbreitete Technik ist das Infinity-Scrolling, wobei weitere Ergebnisse nachgeladen werden, wenn der Nutzer das Ende der Liste erreicht hat. Dieses Prinzip lässt sich auch auf andere Elemente anwenden, die erst sichtbar werden, wenn der Benutzer sich in dem betreffenden Bereich befindet. Beispielsweise Produktempfehlungen, Bewertungen, zuletzt gesehene Artikel etc. Das primäre Ziel ist, die Basisinformationen schnell zur Anzeige zu bringen und weitere Elemente erst nachträglich nachzuladen.
Volatile Requests
Die Analyse von volatilen Use Cases gestaltet sich meistens – bedingt durch das nur sporadische Auftreten – als aufwändiger und komplizierter. Allerdings ist hier ebenfalls nach dem gerade vorgestellten Muster vorzugehen. Um Kandidaten zu identifizieren kann man im ersten Schritt die Antwortzeiten nach dem 95%-Perzentil sortieren. Hierdurch erhalten wir eine Liste mit Anwendungsfällen, die eine schlechte Worst-Case-Performance aufweisen. Die Fragestellung, die hier zu lösen ist, besteht darin, die Varianz der Antwortzeiten zu begründen. Als Datengrundlage kann man hier die Call Trees derselben Requests mit unterschiedlicher Laufzeit gegeneinander stellen. Die Gründe können von den unterschiedlichen Inhalten der Parameter bis hin zur veränderten Auslastung der zuliefernden Sekundärsysteme gehen. Es hat sich aber gezeigt, dass es häufig an unterschiedlichen Eingangsparametern liegt. Hier lohnt ein genauerer Blick zu Beginn. Steigt die Volatilität erst mit erhöhter Systemlast, so deutet es meist auf Synchronisationsprobleme, ausgelastete Ressourcenpools oder Garbage Collections hin.
Requests mit hoher Gesamtausführungszeit
Sollte man die Möglichkeit haben, proaktiv tätig zu werden, so kann man sich den Requestgruppen mit hoher Gesamtausführungszeit zuwenden. Selbst kleine Optimierungen können sich in Summe beachtlich bemerkbar machen. Jedoch gilt auch hier, nicht zu viele Optimierungen zur selben Zeit vorzunehmen!
Werkzeuge
Für das Tuning ist ein gutes Monitoring-Werkzeug unerlässlich. Kommerzielle Lösungen sind oft teuer und bestehende OS-Projekte sind meist nicht speziell für Webanwendungen ausgelegt bzw. sind für den Einsatz im Cluster nicht so gut geeignet. Aus dieser Notwendigkeit heraus haben wir ein OpenSource-Werkzeug Stagemonitor erstellt [1]. Dieses kann während der Entwicklung, der QS- und Produktions-Phase eingesetzt werden um die Performance einer Java-Webanwendung zu überwachen und zu optimieren.
Do's und Don'ts
Wir haben uns nun die verschiedenen Punkte angesehen und können daraus einige Do's and Don'ts ableiten:
- Beginne nie zu optimieren, wenn du nicht gemessen hast, was wirklich passiert.
- Messe die Auswirkungen von jeder Veränderung einzeln, daraus folgt...
- Optimiere nie mehrere Dinge gleichzeitig, die Wechselwirkungen sind in den meisten Fällen nicht vorhersehbar.
- Führe Lasttests mit angemessener Hardware durch und nicht mit dem Laptop (außer Laptop geht in Prouktion).
- Erzeuge Lasttests, die so weit wie möglich der Realität entsprechen, keine synthetischen Lasttests.
- Definiere das zu erreichende Ziel mit messbaren Kriterien (SLAs).
- Beende die Optimierung, wenn das Ziel erreicht ist.
- Überwache in allen Stufen die Performance, von der Entwicklung bis zur Produktion.
Mit diesen einfach Grundregeln kann man die meisten Fehler im Bereich Performance vermeiden und beheben. Was wir in diesem Beitrag nicht beleuchtet haben ist das Thema Lasttest [2].
- Stagemonitor
Github Stagemonitor
Überblick über Features und Funktionsweise: Stagemonitor - Lasttest für die Datenbank - Informatik Aktuell