Java Magazin 03/09

Performance first

Autor:

Verteilung und Kommunikation zwischen Anwendungen und Services ist ein wesentliches Konzept moderner Softwarearchitekturen. Um von Verteilung allerdings profitieren zu können, müssen einige Grundlagen beachtet werden, damit man nicht unweigerlich in massive Performance- und Skalierbarkeitsprobleme läuft. In der Entwicklung bleiben diese Probleme oft verborgen. Erst im Lasttest oder Produktivbetrieb stellt man fest, dass die gewählte Verteilungsarchitektur Performance- und Skalierbarkeitsanforderungen nicht erfüllen kann. Es ist daher erforderlich, die Ursachen für die Probleme zu verstehen.

Verteilung ermöglicht die Interaktion verschiedener Anwendungen untereinander – sei es als einfache Point-to-Point-Kommunikation oder verteilt und dynamisch in serviceorientierten Architekturen (SOA). Kommunikation über Systemgrenzen hinweg ist auch ein notwendiger Bestandteil, um Softwaresysteme verteilen und skalieren zu können, sowie die Verfügbarkeit zu erhöhen. Moderne Softwarearchitekturen sind heute ohne verteilte Anwendungen und Services nicht mehr denkbar. Die Java-Plattform nimmt dabei eine zentrale Rolle ein, weil sie durch die weite Verbreitung und gute API- und Produktunterstützung immer häufiger zur Integrationsplattform in Unternehmen wird. Die Szenarien für verteilte Systeme können dabei sehr unterschiedlich sein und reichen von der Integration von Altsystemen wie CICS und IMS, über die Einbindung von Standardsoftware wie SAP bis hin zum Aufruf unternehmensinterner oder externer Services, die von unterschiedlichsten Abteilungen oder externen Partnern bereitsgestellt und gepflegt werden. SOA propagiert diese Ansätze, um die Wiederverwendbarkeit und Flexibilität von Anwendungen und Services zu erhöhen und so schneller auf veränderte Marktbedinungen und Anforderungen reagieren zu können und die Kosten für Entwicklung und Wartung zu reduzieren. Zudem führen technische Trends wie Grid Computing, Virtualsierung und Blade-Systeme mit immer mehr CPU-Kernen dazu, dass viele Java-Anwendungen in Clustern betrieben werden, um Skalierbarkeit und Ausfallsicherheit besser garantieren zu können. Trends wie Cloud Computing zeigen, dass sich verteilte Serviceplattformen in Zukunft noch stärker verbreiten werden. Zudem werden diese Systeme noch dynamischer und flexibler, da im laufenden Betrieb Serviceknoten hinzugefügt werden können. Die beschriebenen Trends führen aber auch dazu, dass gerade die Systemarchitekturen immer komplexer werden und Entwickler nur noch schwer absehen können, welche Konsequenzen einzelne Serviceaufrufe in produktiven Umgebungen haben. Diese Komplexität kann schnell dazu führen, dass der Ressourcenverbrauch (CPU, Speicher, Netzwerk) stark ansteigt und Performance und Skalierbarkeit leiden.

Das Wesentliche bleibt für das Auge unsichtbar

Moderne Remoting-Technologien haben die Implementierung verteilter Anwendungen sehr stark vereinfacht. Details zur darunterliegenden Kommunikation sowie der Server- und Client-infrastruktur bleiben dem Entwickler weitestgehend verborgen. Das Exportieren von Java- Klassen als Services wird beispielsweise durch die Verwendung von Annotationen stark vereinfacht. Der Zugriff auf diese Services erfolgt dann über automatisch genierte Stubs.

Wie Abbildung 1 zeigt, ist das aber nur die Spitze des Eisbergs einer Remoting-Architektur. Ein wesentlicher Teil eines Remoting Stacks ist die Serialisierung der Daten von der Objektrepräsentation auf ein Transportformat. Um diese muss sich der Anwendungsentwickler normalerweise nicht kümmern. Allerdings kann es hier bereits zu wesentlichen Performanceproblemen kommen. Ein ineffizientes Serialisierungsformat bedingt, dass mehr Daten über das Netzwerk gesendet werden, als tatsächlich notwendig ist. Komplexe Objektrepräsentationen und große Datenmengen resultieren in hohem CPUund Speicherverbrauch bei der Serialisierung und Deserialisierung der Daten. Die darunterliegende Infrastruktur und deren Konfiguration haben auch einen wesentlichen Einfluss auf die Performance der Anwendung. Auf der Clientseite sind die wesentlichen Aspekte das Management von Verbindungen zum Server und das darunterliegende Threading-Modell. Die Regeln für die Verwendung von Verbindungen sind equivalent zu Datenbankanwendungen. Der Aufbau einer Verbindung nimmt sehr viel Zeit in Anspruch. Gleichzeitig benötigt eine Kommunikationsverbindung auch wertvolle Systemressourcen. Das Verwenden von Verbindungs-Pools ist hier ein wesentlicher Aspekt. Wichtig ist die richtige Konfiguration. Das Threading-Modell bezieht sich auf die Art, wie Requests abgearbeitet werden. Ein wesentlicher Aspekt ist dabei, ob Abfragen synchron oder asynchron abgearbeitet werden. Synchrone Kommunikation blockiert einen Thread, bis eine entsprechende Antwort empfangen wurde, bei asynchroner Kommunikation wird die Antwort über einen Callback abgearbeitet. Diese bedeutet zwar etwas mehr Programmieraufwand, führt aber zu einem wesentlich effizienteren Nutzen von Ressourcen. Auf der Serverseite ist das wesentliche Performancekriterium die Anzahl der zur Verfügung stehenden Worker Threads. Diese definieren die maximale Anzahl der parallel abarbeitbaren Anfragen.

Last but not least ist das Netzwerk natürlich eine zentrale Komponente in einer verteilten Anwendung. Das Netzwerk ist eine sehr kritische Engpassressource und schränkt neben der Performance hauptsächlich die Skalierbarkeit der Anwendung ein. Gerade dieser Bereich wird bei der Entwicklung oft übersehen, da beim lokalen Testen kein Netzwerk im Spiel ist.

Was darf’s denn sein – unterschiedliche Remoting- Technologien

Zur Implementierung verteilter Kommunikation steht in Java eine große Bandbreite an Möglichkeiten und Technologien zur Verfügung. Schon die Auswahl des Remoting-Protokolls beeinflusst wesentlich Architektur als auch Performance und Skalierbarkeit der Anwendung.

Das älteste und wohl am meisten genutzte Remoting-Protokoll in Java ist RMI (Remote Method Invocation, Abb. 2), das Standardprotokoll für Java-EEAnwendungen. Wie der Name schon vermuten lässt, handelt es sich um eine Möglichkeit, Methoden von Java-Objekten remote aufzurufen. Auf der Serverseite werden Objekte exportiert. Diese können dann von Clients aufgerufen werden. Die Serverobjekte werden multithreaded verwendet. Die Verwaltung des Threadpools übernimmt hier die RMI-Infrastruktur.

Architektur

Die Kommunikation erfolgt über TCP/ IP, als Protokoll wird JRMP oder bei RMI over IIOP das Corba-Protokoll verwendet. Applikationsserverhersteller bieten weitere eigene Protokolle an, die speziell auf Performance optimiert sind. Da zusätzlich noch Clientreferenzen auf Serverobjekte verwaltet werden müssen, wird ein Distributed Garbage Collector zur Verwaltung des Objektlebenszyklus von Serverobjekten verwendet. Neben der starken Kopplung von Client und Server bringt RMI eine Reihe von weiteren Implikationen mit sich. Es unterstützt ausschließlich synchrone Kommunikation mit allen vorab besprochenen Nachteilen. Des Weiteren kann auch bei datenbasierten Services kein Caching verwendet werden, da die Kommunikation auf einem Binärprotokoll basiert. Der Entwickler kann allerdings die Serialisierung und einige Konfigurationsparameter der Infrastruktur beeinflussen. JMS (Java Messaging Service) ist das zweite im Java-EE-Umfeld verbreitete Kommunikationsprotokoll (Abb. 3).

Im Unterschied zu RMI ist JMS ein asynchrones Protokoll. Die Kommunikation basiert entweder auf Queues oder von Topics, wo mithilfe von Listenern auf Nachrichten reagiert werden kann. JMS ist kein klassisches Remote- Procedure-Call-Protokoll, kann aber trotzdem für die Kommunikation zwischen Systemen genutzt werden. Bei einigen ESB-Implementierungen, der Basis für die Kommunikation in einer SOA, wird eine JMS-basierte Middleware verwendet, um Nachrichten zwischen den Services auszutauschen. Durch die asynchronen Eigenschaften von JMS können typische Queuing- Probleme, wie eingangs beschrieben worden, verhindert werden. In vielen Systemen ist es für die Skalierbarkeit entscheidend, dass Ressourcen (z. B. Threads) schnell wieder freigegeben werden können – asynchrone Mechanismen sind dabei oftmals die einzig zuverlässige Möglichkeit. JMS bietet eine Reihe unterschiedlicher Transportformate an. Am häufigsten wird XML zur Kommunikation verwendet. Die Struktur dieser Nachrichten sollte ein wesentlicher Bestandteil der Anwendung sein, da diese eine potenzielle Quelle für Performance- und Skalierbarkeitsprobleme darstellt.

JMS-Architektur

Web Services über SOAP (Abb. 4) haben sich durch verschiedenste WS- *-Standards auch innerhalb von Java EE etabliert. SOAP wurde als Gegenentwurf zu CORBA entwickelt und von Beginn an durch alle großen Hersteller mit getragen. Durch die Interoperabilitätsbemühungen des WS-I ist es heute möglich, auch zwischen unterschiedlichen Technologien Informationen auszutauschen. SOAP ist dabei ein XMLbasiertes RPC-Protokoll, das sehr häufig und nicht ganz zu Unrecht als „Bandbreitenfresser“ bezeichnet wird. Immer häufiger wird REST als Alternative zu SOAP ins Spiel gebracht, das auf HTTP 1.1. basiert und in Java mit JSR-311 eine Standardisierung erfährt. REST ist allerdings weniger ein RPC-Protokoll, sondern sehr ressourcenzentriert und gut geeignet für Zugriff und Manipulation von Ressourcen im Web. Beide Protokolle sind durch das darunterliegende HTTP-Protokoll synchron implementiert. Für SOAP erlaubt es allerdings die WS-Addressing-Erweiterung, auch asynchrone Services zu implementieren. Ein großer Vorteil von REST speziell ist die einfache Möglichkeit, Caching mittels Proxies zu implementieren. REST nutzt hier Mechanismen, die bereits im http-Protokoll vorgesehen und auch von Webbrowsern verwendet werden. Somit lässt sich Caching ohne wesentlichen Mehraufwand implementieren.

Was schief gehen kann – eine Übersicht

Potenzielle Probleme können an drei Stellen auftreten (Abb. 5). Auf der Clientseite sind es schlechtes Interaktionsdesign – sprich zu viele Serviceaufrufe – oder die Wahl des falschen Kommunikationsmusters. Lang laufende synchrone Aufrufe können schnell zu Performanceproblemen führen. Auf der Kommunikationsebene sind eine hohe Netzwerkbelastung durch große Datenmengen oder eine hohe Anzahl an Serviceaufrufen die Hauptprobleme. Auf der Serverseite führen schlecht designte Serviceschnittstellen und falsche Serialisierungsstrategien zu Performance- und Skalierbarkeitsproblemen. Im Folgenden werden diese Problemquellen näher beleuchtet.

Anti-Pattern: Falsches Protokoll

Die Wahl des richtigen Protokolls für die Kommunikation in verteilten Anwendungen und Services hängt vor allem von der Architektur und den Anforderungen, also den Architekturtreibern ab. Bewegt man sich in einem sehr heterogenen Umfeld, wo beispielsweise Mainframe, .NET und Java-Applikationen miteinander kommunizieren müssen, gibt es heute kaum eine standardisierte Alternative zu SOAP. Kommunizieren ausschließlich Java-Anwendungen miteinander, so ist die Verwendung von RMI mit JRMP die beste Möglichkeit der Kommunikation aus Sicht von Performance und Skalierbarkeit. Bei vielen SOA-Implementierungen wird aber SOA gleichgesetzt mit Web Services und SOAP, sodass man immer häufiger Java-Applikationen sieht, die SOAP als RPC-Protokoll nutzen, obwohl daraus eigentlich keine Vorteile resultieren. Durch Messungen in der Praxis und einigen Benchmarks [1] ist der Overhead von SOAP im Vergleich zu RMI-JRMP nicht zu unterschätzen – eine um den Faktor 10 schlechtere Performance und deutlich höherer Speicher und CPU Verbrauch sind dabei keine Seltenheit. Eine auf der JAX durchgeführte Studie zum Thema Java-Performance [2] sieht Web Service mit SOAP sogar als Performance kritischste Technologie im Java-Umfeld. Neben den bereits beschriebenen Java-Standard-Protokollen haben sich aber auch eine Reihe weiterer XML-basierter und binärer Protokolle etabliert. Gerade das binäre Protokoll Hessian stellt eine performante Alternative im Java-Umfeld dar, für das es auch Implementierungen in anderen Programmiersprachen gibt. Nutzt man beispielsweise das Spring-Framework, um POJOs für einen entfernten Aufruf zu exportieren [3], ist es relativ einfach, zwischen den verschiedenen Protokollen zu wechseln, ohne die Implementierung zu verändern. Spring unterstützt dabei RMI, HTTP, Hessian, Burlap, JAX-RPC, JAX-WS und JMS.

SOAP-/REST-Architektur

Anti-Pattern: Zu große Nachrichten

Bei einem Aufruf von entfernten Services werden immer Daten in Form von Nachrichten über unterschiedliche Protokolle ausgetauscht. Im Fall von SOAP sind das beispielsweise XML und bei RMI-JRMP binäre Nachrichten. Bei den meisten Technologien im Java-Umfeld werden in den Nachrichten Daten von Objekten oder die Objekte selbst ausgetauscht. Die Serialisierung der Java-Objekte in das entsprechende Format des Protokolls erfolgt dabei meistens „unterirdisch“ in der Implementierung der Remoting-Technologie. Der Overhead dieser Serialisierung ist proportional zur Größe der Objekte, die übertragen werden sollen. Der Overhead für das Protokoll war in diesem realen Fall mehr als 98 Prozent der gesamten Antwortzeit des Service. Was war passiert? Die Schnittstelle des Authentifizierungsservice verlangte ein Benutzerobjekt, das authentifiziert wird. Das Benutzerobjekt enthielt neben der User-ID und dem Passwort, die für die Authentifizierung notwendig waren, sehr viele weitere Attribute und Referenzen auf Berechtigungen, Firmendaten und sonstigen Objekten, die mit dem Benutzer verknüpft waren. Der SOAP-Serialisierungsmechanismus erzeugte daher eine Nachricht mit mehreren Kilobyte XML-Daten aus dem Objekt, das dann durch den Service geparst und auf die zu erzeugende Benutzerobjektstruktur gemappt werden musste. Die Lösung für diesen Performanceengpass war denkbar einfach: Anstatt das Benutzerobjekt zu übergeben, wurde die Schnittstelle so angepasst, dass eine Authentifizierung mit User-ID und Passwort möglich ist – der Overhead des Protokolls wurde dadurch um Faktoren reduziert – bei gleichem Protokoll. Zudem wurde CPU und Speicherverbrauch deutlich gesenkt. Es ist also neben der Auswahl der richtigen Technologien wichtig, die Nachrichtengröße bei Remote-Aufrufen zu kontrollieren und zu minimieren. Das Design der Schnittstellen ist dabei von zentraler Bedeutung und oft spielt einem hier ein zu generisch konzipiertes Objekt einen Performancestreich.

Anti-Pattern: Verteilte Deployments

Verteilte Java-EE-Anwendungen resultieren Verteilte Java-EE-Anwendungen resultieren in vielen Fällen aus der Zerlegung von einzelnen Anwendungen und Services in unterschiedliche Deployment-Einheiten, die innerhalb eines oder mehrerer, verteilter Application-Server deployt werden. Die Zerlegung hat viele Vorteile, unter anderem eine losere Koppelung der Komponenten und Anwendungen, die dazu führt, dass beispielsweise neue Releases dieser Deployment-Pakete unabhängig von anderen Paketen zur Verfügung gestellt werden können. Ein weiterer Vorteil ist die Möglichkeit, gezielter und besser skalieren zu können, in dem beispielsweise ein hoch frequentierter Service auf eigener Hardware und/oder redundant zur Verfügung gestellt wird. Bei komplexen Anwendungslandschaften und vielen unterschiedlichen Deployment-Einheiten wird das Zusammenspiel der Services und Anwendungen schnell aus dem Auge verloren. Das kann dazu führen, dass zwei stark kommunizierende Anwendungen oder Services verteilt deployt werden, wodurch viele Remote-Aufrufe und ein großer Performance-Overhead erzeugt werden. Bei großen, verteilten Systemen ist es daher wichtig, die Kommunikationshäufigkeit und die Datenmenge der Kommunikation zu analysieren und die Deployments auf Basis der Ergebnisse zu strukturieren. Es kann durchaus sein, dass eine Änderung von einem getrennten zu einem lokalen Deployment dazu führt, dass viele Remote-Aufrufe eingespart werden können, ohne dass man Flexibilität und Skalierbarkeit verliert. Gerade bei statuslosen Services kann es sinnvoll sein, diese mehrfach zu deployen, um Lokalität zur jeweiligen Anwendung herzustellen. In einem konkreten Beispiel führte die mehrfache Installation eines Versicherungsproduktsystems zu einer enormen Einsparung von Ressourcen und verbesserter Performance, weil bei jeder Anfrage mehrmals auf den bis dahin entfernt liegenden Service zugegriffen werden musste.

Typische Problemquellen

Fazit

Die verschiedenen Anti- Patterns zeigen, dass es wichtig ist, Skalierbarkeit bereits in der Konzeptionsphase einer Anwendung als Architekturtreiber zu berücksichtigen, da es schwer und aufwändig sein kann, diese nachträglich mit ansprechender Performance zu integrieren. Eine genaue Analyse der Anwendungen unter produktiven Bedingungen ist dabei unerlässlich, um häufige Remote-Aufrufe und große Nachrichten zu identifizieren.

Links & Literatur

[1] Java Remoting, Protocol Benchmarks: daniel.gredler.net/2008/01/07/ java-remoting-protocol-benchmarks/

[2] Performance-Studie: www.codecentric.de/de/publikationen/studien/

[3] Spring Remoting : static.springframework.org/spring/docs/2.5.x/reference/ remoting.html

Zurück zu den Publikationen

Vollständiger Artikel