Java Magazin 09/09

Performance im Umfeld von Webanwendungen

Autor:

In den letzten Artikeln dieser Reihe haben wir uns eingehend mit unterschiedlichen Bereichen im Umfeld von Softwareperformance auseinandergesetzt. Unsere Themen waren Datenbankzugriffe, verteilte Anwendungen und Garbage Collection. Mit dem nötigen Basiswissen ausgestattet, können wir diese Konzepte nun im Umfeld einer konkreten Anwendungsdomäne (Webanwendungen) anwenden.

Webanwendungen sind eine stetig wachsende Anwendungsgruppe und gewinnen im Enterprise-Umfeld zunehmend an Bedeutung. Dabei haben webbasierte Anwendungen in den letzten Jahren immer mehr klassische Client/Server- und Mainframe-Anwendungen abgelöst. Das ist wohl einerseits auf das einfache Deployment (man braucht nur einen Browser) als auch auf die große Anzahl verfügbarer Webframeworks zurückzuführen, die die Entwicklung einfacher und effizienter machen. Zunehmend spielt aber auch die „Öffnung“ der Anwendungen für unterschiedliche Benutzergruppen eine Rolle – so stehen Anwendungen neben Mitarbeitern auch den Kunden, Partnern und Lieferanten zur Verfügung.

Die Bandbreite bei modernen Webanwendungen reicht von der einfachen Dateneingabe und Manipulation (CRUD) bis hin zu komplexen Benutzerschnittstellen, die klassische Rich Clients ersetzen. Durch moderne Technologien und Frameworks speziell im Ajax-Umfeld schließen die Webanwendungen auch funktional zu Rich Clients auf. Diese Anwendungen sind dann entweder für eine begrenzte Benutzeranzahl als Intranetanwendungen oder aber für jedermann über das Internet zugänglich. Performance stellt hier natürlich ein zentrales Kriterium dar – vor allem bei frei zugänglichen Anwendungen im Internet, da dort die Anzahl der Benutzer deutlich weniger vorhersehbar und damit planbar ist als im eigenen Unternehmen.

Aktuelle Trends zeigen, dass sich der Browser zudem immer mehr zur Anwendungsplattform entwickelt. War er ursprünglich einmal zum Anzeigen statischer HTML-Seiten gedacht, so läuft heute bereits ein nicht unwesentlicher Teil an Anwendungslogik in JavaScript im Browser. Ajaxbasierte Anwendungen sind immer häufiger anzutreffen und werden auch in Zukunft weiter an Bedeutung gewinnen. Die Entwicklung von Google V8 [1] zeigt die zunehmende Bedeutung von JavaScript. Web Sockets (die auch Teil der HTML-5-Spezifikation sind) erlauben zusätzlich eine verbesserte Kommunikation zwischen Browser und Serversystemen. Zudem haben sich Alternativen zur klassischen HTML/JavaScript-Technologie entwickelt – Adobe Flash, Microsoft Silverlight und JavaFX erlauben funktionsreiche Anwendungen und ermöglichen die Kommunikation mit serverseitigen Services. Während sich auf der Framework- und Clientseite einiges getan hat, blieb die technologische Basis – das HTTP-Protokoll – unverändert.

performanceprobleme bei webanwendungen

Drill-down-Analyse von Performanceproblemen

Von Anfragen und Antworten – Das HTTP-Protkoll

Die Basis für die Kommunikation in Webanwendungen ist das HTTP-Protokoll. Ursprünglich dazu entwickelt, um Dokumente zu übertragen, basiert es auf einem einfachen Request/Response-Mechanismus. Der Browser schickt eine Anfrage und wartet so lange, bis diese vom Server verarbeitet wird. Hierbei handelt es sich um eine rein synchrone Kommunkation. Das führte dazu, dass auch die meisten Frameworks für eine rein synchrone Abarbeitung von Abfragen ausgelegt sind. Das im Java-Umfeld am weitesten verbreitete Servlet API sieht hierfür die Methoden doGet und doPost vor. In diesen Methoden findet die gesamte Abarbeitung der Abfragen statt. Jede Anfrage wird dabei innerhalb des Webcontainers von einem eigenen Thread verarbeitet. Eine definierte Möglichkeit, die Abarbeitung zu pausieren und den Thread für eine andere Anfrage zu verwenden, ist im heutigen Standard nicht möglich. Gerade bei hoch frequentierten Anwendungen kann das zu einer Einschränkung der Skalierbarkeit führen. Durch die limitierte Verfügbarkeit von Threads kommt es dann zu einem Queuing der Anfragen und die Performance nimmt rapide ab.

Mit dem Servlet API 3.0 wird die Schnittstelle erstmals erweitert und erlaubt es nun, die Abarbeitung von Servlet Requests anzuhalten, um sie dann später wieder aufzunehmen. Das führt zu einer deutlich effizienteren Nutzung von Threads in Servlet-Containern und letztendlich verbesserter Skalierbarkeit. Im Moment ist der JSR 315 allerdings noch in Arbeit. Viele Servlet-Container und JEE-Server bieten aber schon heute ähnliche Möglichkeiten an (z. B. asynchrone Servlets in Oracle Weblogic). Speziell wenn man mit sehr langen Request Queues zu kämpfen hat und Anwendungen die meiste Zeit mit Warten verbringen, kann man mit dieser Erweiterung wesentliche Optimierungen vornehmen. Gerade bei Webanwendungen, die externe Systeme mit langlaufenden Aktionen aufrufen (z. B. IMS- oder CICS-Transaktionen), kann die Wiederverwendung der Threads von großem Vorteil sein.

Wenn alles langsam ist

Wenn es zu Performanceproblemen in einer Webanwendung kommt, erkennt man das zuerst am schlechten Antwortzeitverhalten der Anwendung. Die Gründe dafür können sehr vielfältig sein.

In Abbildung 1 ist die Abarbeitung eines Requests einer Anwendung inklusive möglicher Ursachen für Performanceprobleme zu sehen. Im Browser können die Ursachen beispielsweise im ineffizienten Rendering oder JavaScript-Code liegen. Auf der Netzwerkebene können Datenübertragungsprobleme aufgrund von hohen Latenzzeiten oder großen Datenmengen verantwortlich sein. Auf der Serverseite können Garbage-Collector-Probleme, falsch konfigurierte Thread oder Connection Pools, Datenbankzugriffe oder Web-Service-Aufrufe für Probleme verantwortlich sein.

Das lässt vermuten, dass die Diagnose der Probleme sehr kompliziert ist. Tatsächlich erlaubt ein strukturiertes Vorgehen mit passender Werkzeugunterstützung eine sehr schnelle Eingrenzung der Probleme. Die Werkzeugunterstützung reicht hier von frei verfügbaren Java-Bordmitteln bis hin zu professionellen Lösungen.

Wir stellen nun ein Vorgehen vor, das darauf abzielt, Probleme Schritt für Schritt einzugrenzen. Als Erstes wollen wir herausfinden, ob unser Problem im Browser, am Netzwerk oder im Server liegt. Idealerweise kann man alle drei Komponenten integriert betrachten. In der Praxis hat man speziell bei Internetanwendungen nicht die Möglichkeit, die Diagnose bereits im Browser des Nutzers zu beginnen, oder dieser müsste zuerst überzeugt werden, ein entsprechendes Plug-in zu installieren. Am Einfachsten ist es, die Probleme auf dem Server zu suchen, da man hier die volle Kontrolle besitzt. Für Regeln und Tipps zum clientseitigen Tuning von Webseiten empfehlen wir die Webseite Best Practices for Speeding Up Your Web Site von Yahoo [4] mit vielen sehr nützlichen Hinweisen, um HTML-Code zu optimieren. Mit YSlow [5] stellt Yahoo auch ein Firefox-Plug-in zur Verfügung, das hilft, die besprochenen Probleme in den eigenen Webseiten zu analysieren.

Serverseitige Probleme erkennen wir an langsamen Antwortzeiten des Servlets. Zusätzlich können wir mit entsprechenden Werkzeugen auch noch die übertragene Datenmenge ermitteln, um hier potenzielle Netzwerkengpässe zu erkennen. Haben wir lange Serverantwortzeiten als Problemquelle identifiziert, können wir das durch die Analyse einiger Metriken weiter eingrenzen. Hierbei unterscheiden wir zunächst zwischen hohem oder niedrigem CPU-Verbrauch. Ist er niedrig, ist das ein Zeichen dafür, dass sich die Anwendung im Wartezustand befindet. Gründe können verteilte Serviceaufrufe, Datenbankzugriffe, Überlastung von Connection Pools oder synchronisierter Datenzugriff sein. Die vergangenen Artikel der Performanceserie behandeln diese Themen im Detail. Sollten wir aber einen sehr hohen CPU-Verbrauch in der Anwendung feststellen, so muss dieser weiter eingegrenzt werden. Ein häufiges Problem ist hier ein falsch konfigurierter Garbage Collector. Das lässt sich durch die Analyse der entsprechenden JVM-Metriken einfach herausfinden. Kommt der hohe CPU-Verbrauch allerdings von Anwendungskomponenten, so müssen sie weiter analysiert werden. Hiefür reicht die Toolpalette von einfachen Profilern bis hin zu professionellen Werkzeugen.

Abbildung 2 zeigt einen Komponentenüberblick einer Webtransaktion nach CPU-Verbrauch auf Komponenten- als auch Methodenebene.

Vorbeugen ist besser als heilen. Deshalb wollen wir uns auch mit den häufigsten Problemmustern in Webanwendungen auseinandersetzen. Im Idealfall können wir diese schon erkennen, bevor es tatsächlich zu Problemen kommt und müssen uns erst gar nicht mit der Diagnose von Produktionsproblemen auseinandersetzen. Wir wollen deshalb die wichtigsten Problemmuster im Detail betrachten.

Antipattern – Zu viele Requests

Grundsätzlich gilt bei Webanwendungen wie bei jeder verteilten Kommunikation, dass die Anzahl der Interaktionen so gering wie möglich gehalten werden soll. Eine hohe Anzahl führt zu hoher Netzwerk- und gegebenenfalls auch hoher Serverbelastung. Die Gründe können vielschichtig sein. Speziell Ajax-Anwendungen führen sehr oft zu einer hohen Anzahl von Serverinteraktionen. Hierdurch soll die Interaktivität der Anwendung erhöht werden. Hier ist bereits in der Entwicklung darauf zu achten, dass die Anzahl der Abfragen so gering wie möglich gehalten wird. Werden z. B. dynamisch Teile einer Webseite bei Benutzerinteraktionen neu erstellt, kann ein falscher Event Handler (z. B. onKeyPressed statt onBlur) zu sehr vielen Serveranfragen führen.

Ein weiterer, sehr häufig vernächlässigter Punkt ist das explizite Arbeiten mit dem EXPIRES Header in HTTP. Wir haben diesen Punkt bereits beim Thema REST gestreift [6]. Eine Webanwendung besteht in den meisten Fällen aus statischem und dynamischem Content. Der EXPIRES Header hilft dem Browser zu ermitteln, ob und wann eine Ressource neu zu laden ist. Im Idealfall wird statischer Content vom Browser nur einmal geladen. Werden allerdings statische Ressourcen vom Webserver mit falschem Ablaufdatum an den Client geschickt, so kann es schnell zum vielfachen Anfordern von Ressourcen kommen. Das setzt den Webserver unter unnötige Last, belastet das Netzwerk und führt zu langen Renderzeiten im Browser. Je nach Netzwerktyp kann auch die Latenzzeit entscheidend für die Performance sein. So liegt sie im Mobilfunk mit GPRS bei ca. 500 ms. Werden also beispielsweise 30 Bilder beim Server angefragt, resultiert das in einer Antwortzeit von 15 Sekunden. Ohne EXPIRES Header fragt der Browser auch statische Ressourcen an, die er im lokalen Cache hat – der Server beantwortet diese Anfragen in der Regel mit dem Statuscode HTTP-304 – „not modified“. Bei unserem GPRS-Beispiel mit 30 Bildern würde das bedeuten, dass auch dann 15 Sekunden Antwortzeit entstehen würden, wenn alle Bilder im Browser-Cache liegen. Ob der EXPIRES Header richtig gesetzt wurde, lässt sich mittels eines einfachen Proxies und einer Analyse des Headers feststellen. Content Expiration ist allerdings nicht nur bei statischen Ressourcen nützlich. Ein sehr großes Problem bei vielen Anwendungen sind mehrseitige Suchergebnisse. Sie können – wenn sehr viel gesucht wird – auch die Datenbank massiv belasten. Sie lassen sich ebenfalls vom Browser cachen. Setzt man hier die Gültigkeit auf einige Minuten, können oft sehr viele Datenbankabfragen vermieden werden. Handelt es sich um Abfragen, die von vielen Benutzern ausgeführt werden, so muss zusätzlich der CACHE-CONTROL Header auf PUBLIC gesetzt werden, damit wir auch Proxy-Servern (und nicht nur dem Browser) erlauben, den Inhalt zu cachen.

Antipattern – Falsches State Handling

State Handling ist ein zentraler Punkt in Webanwendungen, deren Daten über einen gewissen Zeitraum zu verwalten in jeder Enterprise-Java-Anwendung unerlässlich ist. In Webanwendugen finden wir drei Grundtypen von Zuständen:

  • Request State: Zustand, der von einem Request zum nächsten benötigt wird. Frameworks wie JSF benötigen diesen beispeilsweise, um Änderungen in Controls überprüfen zu können.
  • Conversational State: Das sind Zustandsinformationen, die über mehrere Requests gespeichert werden müssen. Ein Beispiel sind Daten, die über mehrere Formulare eingegeben werden und zwischengespeichert werden müssen.
  • Session State: Zustandsinformation, die für eine gesamte Benutzersitzung verfügbar sein muss – z. B. Authentifizierungsdaten eines Benutzers.

Zustandsinformationen sollten so klein wie möglich gehalten und so schnell wie möglich wieder freigegeben werden. Neben dem offensichtlichen Speicherverbrauch ergeben sich durch das Halten von Zustandsinformationen Skalierbarkeitsprobleme. Zustände müssen in Clustern zwischen den Clusterknoten repliziert werden. Das bedeutet zusätzlichen Netwerk- und CPU-Overhead und führt zu Performanceverschlechterungen in der Anwendung. Replikation von Zustandsinformation bezieht sich auf alle Informationen, die in der Benutzersession gehalten werden. Man spricht von aktiver Replikation, wenn alle Änderungen immer im Cluster repliziert werden, und von passiver Replikation, wenn diese nur teilweise repliziert werden, um Ausfallssicherheit zu gewährleisten und sonst bei Bedarf geladen werden.

Sehr oft wird die Möglichkeit, Zustände im Client – also im Browser – zu speichern, übersehen oder nicht entsprechend ausgenutzt. Die einfachste Möglichkeit sind hier Cookies. Kleine Datenmengen, z. B. Benutzererkennungen, können verschlüsselt in einem Cookie gespeichert werden. Diese Information ist dann serverseitig immer verfügbar, ohne den Zustand auf dem Server speichern zu müssen. Ajax-Anwendungen bieten zudem sehr gute Möglichkeiten, Conversation State im Browser abzulegen. Hierbei werden erst am Ende einer Transaktion Daten zum Server übertragen. Der Server kann also zustandslos arbeiten und man gewinnt zusätzlich an Ausfallssicherheit, da Sticky Sessions, also Benutzersitzungen Benutzersitzungen, die auf einen bestimmten Server gebunden sind, vermieden werden. Beim Request State hat man die Qual der Wahl zwischen serverseitigem oder clientseitigem State Handling. Beide haben Vor- und Nachteile. Beim clientseitigen State Handling werden Daten jedes Mal zum Server und wieder zurück geschickt. Das erhöht die Netzwerkbelastung, was zu langen Antwortzeiten im Browser führen kann. Beim serverseitigen State Handling wird zusätzlicher Speicher am Server benötigt und man läuft in Replikations- und Stickyness-Probleme hinein. Idealerweise können Anwendungen mit wenig Zustandsinformation auskommen. Speziell wenn keine Events aufgrund von geänderten Daten ausgelöst werden müssen, sind diese Daten oft nicht notwendig. Auch hier gilt die alte Weisheit „Der beste State ist der, den man gar nicht benötigt“.

Antipattern – Schlechte GC-Konfiguration

Wie bereits im letzten Artikel angesprochen, ist die Konfiguration des Garbage Collectors zentral für gute Performance einer Anwendung. Webanwendungen haben spezielle Anforderungen an den Garbage Collector, die von der Standardkonfiguration nicht abgedeckt werden. Es werden sehr viele Objekte erzeugt und nach Beenden eines Requests auch wieder freigegeben. Das bedeutet, dass man mit einer sehr großen Young Generation arbeiten sollte und speziell auch den Eden-Bereich groß konfigurieren kann, um unnötige Full Collections zu vermeiden [7].

Antipattern – Langlaufende synchrone Aufrufe

Wie bereits erwähnt, erfolgt die Abarbeitung von Abfragen im Webumfeld synchron. Das trifft oft auch auf Aufrufe zum Backend-System zu. Als Programmierer denkt man zudem auch prozedural: Zuerst dieser Service, dann dieser usw. In vielen Fällen können Aufrufe an Fremdsysteme asynchron durchgeführt und parallelisiert werden. Hierbei wird sehr viel Zeit mit Warten auf Antworten verbracht, die durch asynchrone Abarbeitung vermieden werden kann. Das wirkt sich sowohl auf die Performance als auch auf die Skalierbarkeit der Anwendung positiv aus. Der einzige Nachteil dieses Ansatzes ist der damit verbundene zusätzliche Implementierungsaufwand. Diesen wird man natürlich nicht „auf Verdacht“ investieren. Aus Architektursicht ist es wichtig, die Anwendung bereits vorab so zu designen, dass von einem synchronen auf einen asynchronen Abarbeitungsansatz umgestellt werden kann.

Antipattern – Fehlendes oder falsches Caching

Caching ist wohl eines der am weitesten verbreiteten Rezepte für Performanceoptimierung. Wie bereits angesprochen, gibt es bei Webanwendung die Möglichkeit, Abfrageergebnisse sowohl für statischen als auch für dynamischen Content zu cachen. Als Caches eignen sich der Browser, Proxies oder spezielle Caching-Server. Auch Content-Management-Systeme bieten in der Regel ausgefeilte Caching-Konzepte, um bei dynamischen Seiten den generierten Anteil zu reduzieren. Hinzu kommt noch Caching von Daten auf der Anwendungsebene. Der Flaschenhals vieler Internetanwendungen ist die Datenbank. Gerade im Datenbankbereich kann man durch intelligentes Caching von häufig gelesenen Daten die Skalierbarkeit der Anwendung massiv erhöhen. Im Artikel zu O/R Mapping haben wir bereits Caching-Strategien von Frameworks wie Hibernate erklärt. Ob man in Clustern allerdings zu verteilten Caches greift, ist eine andere Frage. Hier ergibt sich dieselbe Problemstellung wie beim verteilten State Handling. Man sollte hier immer genau den Performance-Overhead des Caches analysieren. In einigen Fällen ist es sogar schneller, Daten aus der Datenbank zu laden, anstatt diese über einen Cluster Cache zu verteilen. Um zusätzlich noch zunehmende Synchronisationszeiten in Read-only Caches zu vermeiden, können diese Daten bereits beim Anwendungsstart geladen und im Speicher gehalten werden. Synchronisation oder dynamisches Nachladen von Daten ist dann nicht mehr notwendig. Um aktuelle Daten gewährleisten zu können, sollte man die Caches regelmäßig invalidieren. Das kann auch programmatisch zu Zeiten passieren, in denen wenig oder keine Last am System ist. Genau so schlimm wie fehlendes Caching ist auch übertriebenes Caching. Es ist kein Allheilmittel für Skalierbarkeitsprobleme. Sind Caches falsch konfiguriert oder werden sie in Szenarien verwendet, in denen sich Daten oft ändern, führen diese sogar noch zu zusätzlichem Overhead.

Optimieren der fachlichen Logik

Eine nicht zu unterschätzende Form der Optimierung von Webanwendung ist die Anpassung der Fachlichkeit, um die Performance zu steigern. Das ist natürlich nur dann möglich, wenn die Anforderungssteller einwilligen bzw. von der Notwendigkeit aus Performancesicht überzeugt werden können. Ein Praxisbeispiel ist die Anzeige einer Länderliste bei der Erfassung der Kundenadresse. Eine vollständige Liste aller Länder kann schnell mehrere Kilobyte groß werden und muss bei jeder Anfrage zum Browser übertragen werden – je nach Implementierung muss die Liste auch noch aus einer Datenbank gelesen werden. Oft ist es aber so, dass nur sehr wenige Länder benötigt werden – so werden beispielsweise deutsche Versicherungen über ihren deutschen Vertrieb hauptsächlich in Deutschland lebende Kunden versichern. Hier könnte die Länderliste im Standardfall weggelassen werden und das Land fix auf Deutschland vorbelegt werden – nur in Ausnahmefällen könnte der Anwender diese Einstellung mit einem zusätzlichen Request ändern und müsste nur dann die Länderliste laden und übertragen.

Fazit

Mit der zunehmenden Bedeutung und Verbreitung von Webanwendungen wird auch Performance ein zentrales Thema. Einerseits können neue Technologien wie Ajax zu Performanceproblemen führen, anderseits finden wir typische Problembereiche wie Datenbankzugriffe und Interaktion mit Backend-Systemen. Obwohl die Problemanalyse anfangs sehr komplex wirkt, sind diese mit einem strukturierten Vorgehen leicht zu finden. Wenn man weiter während der Implementierung darauf achtet, typische Antipatterns zu vermeiden, können Webanwendungen hochperformant und -skalierbar implementiert werden.

Links & Literatur

[1] Google V8 JavaScript Engine: http://code.google.com/p/v8/
[2] Web-Socket-Spezifikation: http://dev.w3.org/html5/websockets/
[3] Servlet API 3.0: http://jcp.org/en/jsr/detail?id=315
[4] Yahoo-Webperformance: http://developer.yahoo.com/performance/rules.html
[5] YSlow: http://developer.yahoo.com/yslow/
[6] Reitbauer, Alois; Novakovic, Mirko: „RESTlos glücklich“, in Java Magazin 5.09, S. 56
[7] Reitbauer, Alois; Novakovic, Mirko: „Der Garbage Collector – Das unbekannte Wesen“, in Java Magazin 8.09, S. 65

Vollständiger Artikel