Zieleinlauf mit Java

In der Märzausgabe ging iX mit einem kleinen Test der Frage nach, ob und unter welchen Umständen Perl, PHP oder C schneller arbeiten. Was vielen Lesern bei diesem Vergleich fehlte, waren Varianten mit Java. Dieser Beitrag schließt diese Lücke und erweitert die Testreihe um Servlet- und JSP-Lösungen.

In Pocket speichern vorlesen Druckansicht 35 Kommentare lesen
Lesezeit: 8 Min.
Von
  • Dominik Gruntz
  • Hans-Peter Oser
Inhaltsverzeichnis

In einem Projekt standen die Autoren vor der Frage, ob sie eine Lösung auf der Basis von PHP oder Java realisieren sollten. Der Beitrag [1] in iX zur Performance von Skriptsprachen und C kam gerade recht, leider fehlten die Zahlen für Java. An dieser Sprache klebt der Makel, dass sie auf Grund des Garbage Collector langsam sei. Ein Test sollte zeigen, ob dieses Vorurteil immer noch seine Berechtigung hat.

Zum Einsatz kam wieder das einfache Beispiel aus dem PHP-Perl-C-Vergleich, in dem ein kurzes 100-zeiliges beziehungsweise ein 1000-zeiliges HTML-Dokument generiert und ausgeliefert wird. Ob solche Tests sinnvoll sind oder nicht, soll hier nicht Gegenstand der Betrachtung sein; als ein Entscheidungsbaustein für die zu wählende Projektsprache schien er aber hinreichend zu sein.

Prozentuale Performance der Implementierungen im Verhältnis zur Auslieferung eines statischen Dokuments, das mit 100 angesetzt ist.

Die Messung wurde wiederum mit ab2 (Apache Bench Version 2) durchgeführt, mit den Optionen -c 10 -n 1000, also 1000 Durchläufe bei zehn gleichzeitigen Aufrufen. Apache Bench lief auf demselben Rechner wie der Webserver. Als Testrechner kam ein System mit zwei Xeon-2,4-GHz-CPUs und 2 GByte RAM unter Suse Linux (Kernel 2.6.8-24.11-smp) zum Einsatz. Als Webserver diente der Apache (Version 2.0.50), als Servlet Container Tomcat (Version 5.0.27), die Tests liefen mit Java 5 (jdk-1.5.0_01) und dem Default-GC als Garbage Collector. Um die Resultate nicht zu verfälschen, wurde nach jeder Testsequenz der Speicher mit einem expliziten GC-Aufruf aufgeräumt. Der Servlet-Zugriff erfolgte in Vorversuchen in einer Variante direkt über Tomcat, in einer anderen über das Apache-Modul mod_jk. Letzteres stellte sich als rund 30 Prozent langsamer heraus, darum beziehen sich die im Artikel erwähnten Servlet-Messungen immer auf den Tomcat-Direktzugriff.

Als erste Variante sollte ein einfaches Servlet GET-Anfragen mit der Methode doGet beantworten (Listing 1). Die generierte Seite enthält dieselben META-Anweisungen wie die Beispiele in [1]; die entsprechenden Anweisungen sind in den Listings aus Platzgründen durch drei Auslassungspunkte ersetzt. Die Zeilenschaltungen sind direkt in den Strings kodiert, da diese Variante effizienter ist als die Verwendung von println. Die Variable max gibt die Größe des Dokumentes (10 oder 100) an, ihr Wert wird in der init-Methode des Servlets gesetzt und kann im web.xml-Deskriptor definiert werden.

Mehr Infos

Listing 1

Servlet mit Ausgabe einzelner Strings

public void doGet 
(HttpServletRequest request, HttpServletResponse response)
throws IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.print("<html><head>\n");
...
out.print("</head><body>\n");
for(int j = 0; j < max; j++){
for(int i = 0; i < 10; i++){
out.print("Dies ist Zeile " + j + "/" + i + "<br>\n");
}
}
out.print("</body></html>\n");
out.close();
}

Dieses Servlet liefert die kleinen Dokumente (100 Zeilen) im Durchschnitt in 4 ms aus, die großen (1000 Zeilen) in 10,4 ms. Davon sind etwa 3,2 ms konstanter Aufwand, der Rest hängt linear von der Größe der generierten Seite ab.

Nun weiß der geübte Java-Programmierer, dass man Strings nicht mit dem +-Operator verknüpfen darf. Besser ist, die Strings mit einem StringBuffer oder seit Java 5 mit einem StringBuilder aufzubauen. Letzterer ist effizienter, da die Zugriffsmethoden nicht synchronisiert sind. Listing 2 zeigt den Code für eine optimierte Servlet-Variante angegeben, bei dem die gesamte Resultatseite zuerst in einen StringBuilder geschrieben wird, dessen Initialgröße so festgelegt ist, dass die gesamte Seite Platz hat. Weiterer Vorteil dieser Lösung: nur ein Schreibzugriff auf den Output-Stream. Man ist so nicht davon abhängig, ob dieser gepuffert ist oder nicht.

Mehr Infos

Listing 2

Optimierte Servlet-Implementierung mit StringBuilder

public void doGet 
(HttpServletRequest request, HttpServletResponse response)
throws IOException{
response.setContentType("text/html");
StringBuilder buf = new StringBuilder(240*max+100);
buf.append("<html><head>\n");
...
buf.append("</head><body>\n");
for(int j = 0; j < max; j++){
for(int i = 0; i < 10; i++){
buf.append("Dies ist Zeile ");
buf.append(j);
buf.append("/");
buf.append(i);
buf.append("<br>\n");
}
}
buf.append("</body></html>");
PrintWriter out = response.getWriter();
out.println(buf);
out.close();
}

Mit dieser Variante erreicht man eine Verbesserung beim von der Größe des Dokuments abhängigen Aufwand um den Faktor 1,7, was sich - zusammen mit dem konstanten Aufwand von 3,2 ms - beim großen Dokument in einer Verbesserung der Ausführungsgeschwindigkeit um etwa 40 Prozent, beim kleinen um etwa 9 Prozent niederschlägt. Verglichen zum statischen Zugriff auf das Dokument erreicht dieses Servlet einen Durchsatz von 43 Prozent beim großen und 81 Prozent beim kleinen Dokument.

Die Generierung von HTML-Code in einem Servlet ist unhandlich und unter Wartbarkeitsaspekten unschön. Darum sollte man HTML-Anweisungen und Java-Code trennen. Ein erster Ansatz dazu sind die Java Server Pages (JSP). JSP-Seiten sind HTML-Seiten, die Java-Code enthalten können. Der Webserver übersetzt Java Server Pages in Servlets, daher erwarten wir ähnliche Resultate wie bei der Servlet-Lösung.

In Listing 3 ist die JSP-Implementierung des kleinen Beispiels zu sehen. Der Aufwand bei den großen Dokumenten ist minimal geringer als bei den einfachen Servlets (die Strings werden nicht mit dem +-Operator verknüpft sondern über mehrere print Aufrufe ausgegeben), bei den kleinen jedoch deutlich schlechter - hier macht sich JSP-Overhead bemerkbar.

Mehr Infos

Listing 3

Java Server Pages zur Trennung von Programm und HTML

<%@ page contentType="text/html" %>
<html><head>
...
</head><body>
<%
int max = Integer.parseInt(getInitParameter("max"));
%>
<%
for(int j=0; j<max; j++){
for(int i=0; i<10; i++){
%>
Dies ist Zeile <%=j%>/<%=i%><br>
<%
}
}
%>
</body></html>

Etwas ungewohnt ist die Mischung von HTML- und Java-Code. Damit der Designer nur noch HTML-ähnliche Anweisungen sieht, ist die JSP Standard Tab Library (JSTL) definiert worden, die JSP-Basisfunktionen (Schleifen, Bedingungen etc.) in einfachen Tags zur Verfügung stellt. Eine Formulierung der Lösung mit Hilfe der JSTL zeigt Listing 4. Wie in der Grafik zu sehen ist, dauert die Generierung und Auslieferung des großen Dokuments 7,5-mal so lange wie die des kleinen, das heißt: Der Aufwand steigt sehr stark mit der Größe des Dokuments an. Im Vergleich zum statischen Zugriff auf das 1000-Zeilen-Dokument benötigt die JSP/JSTL Lösung rund 30-mal so lange.

Mehr Infos

Listing 4

JSP-Lösung mit JSTL

<%@ page contentType="text/html" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html><head>
...
</head><body>
<c:forEach var="j" begin="0" end ="9">
<c:forEach var="i" begin="0" end ="9">
Dies ist Zeile <c:out value="${j}"/>/<c:out value="${i}"/><br>
</c:forEach>
</c:forEach>
</body></html>

Eine andere Methode, um Java- und HTML-Code zu trennen, ist die Verwendung einer Template-Engine. Die Autoren haben in vielen Projekten Erfahrungen mit Velocity [2] gesammelt und das Beispiel wie folgt implementiert: In einer Unterklasse von VelocityServlet (vgl. Listing 6) wird das Template geladen und im Kontext die Werte abgelegt, auf die im Template zugegriffen wird (die obere Schranke max). Das Template (Listing 5) sieht in diesem Fall ähnlich aus wie die JSP-Lösung, außer dass für den Zugriff auf die Java-Objekte und für die Formulierung der Kontrollstrukturen die Velocity-Template-Sprache (VTL) zum Einsatz kommt. Der Aufwand für das kleine Dokument ist etwa 25 Prozent geringer als jener für die JSP-Lösung, die Velocity-Lösung fällt jedoch bei größeren Dokumenten ab.

Mehr Infos

Listing 5

Velocity-Template

<html><head>
...
</head><body>
#foreach ($j in [0..$max])
#foreach ($i in [0..9])
Dies ist Zeile $j/$i<br>
#end
#end
</body></html>
Mehr Infos

Listing 6

Velocity-Servlet

public Template handleRequest 
(HttpServletRequest request, HttpServletResponse response, Context context) {
// default content type is "text/html"
context.put("max", new Integer(max-1));
if (template == null){
try {
template = getTemplate("templates/example.vm");
}
catch(Exception e){
System.out.println(e);
}
}
return template;
}

Mit der Velocity gab es zudem generell Probleme bezüglich Stabilität bei hoher Last.

Um richtige Bezugswerte zu haben, wurden die Tests auch mit PHP und CGI wie in [1] ausgeführt. Bemerkenswert daran sind die FastCGI-Resultate auf dem eingesetzten Multiprozessorsystem: Man muss unbedingt darauf achten, mehrere Instanzen des Programms zu laden. Auf dem Testrechner führten vier gleichzeitige Instanzen zur optimalen Leistung - das Hyperthreading hat hier etwas gebracht. Ohne diese mehrfachen Instanzen war die FastCGI-Leistung um über 30 Prozent schlechter!

Die Abbildung zeigt die Performance der einzelnen Lösung relativ zur Auslieferung einer statischen Seite. Dort nicht ersichtlich ist die Auslastung: Während die CGI-Scripts das System vollständig beschäftigten (0 % idle time), ließ sich das System mit den Servlets nur zwischen 70 Prozent (Servlet optimiert) und 85 Prozent (JSP) auslasten. Auch das Anheben der Priorität des Tomcat-Prozesses änderte daran nichts. Möglicherweise handelt es sich um ein SMP-Problem der verwendeten JVM.

Der Test hat aber deutlich gezeigt, dass sich die Java-Lösungen durchaus sehen lassen können. Sie sind nicht nur performanter als das in [1] mit 39 beziehungsweise 20 Prozent gemessene Perl, das aus anderen Gründen für das Projekt nicht in Betracht kam, sondern auch schneller als FastCGI. Das dürfte zum einen am bei FastCGI notwendigen Umschalten der Adressräume, zum anderen an der Hotspot-Optimierung der Servlets liegen. PHP schied übrigens, um auf die Eingangsfrage zurückzukommen, nach diesen Messungen als Projektsprache aus.

Dr. Dominik Gruntz
ist an der Fachhochschule Aargau/Nordwestschweiz als Professor für Software-Architektur & Design tätig.

Hans-Peter Oser
ist ebendort Professor mit dem Spezialgebiet Systeme.

[1] Jürgen Seeger, Zieleinlauf: Auslieferungsmethoden im Performancevergleich, iX 3/05, S. 94

[2] Andreas Pacek, Schraubhilfe: Apache-Projekt Velocity, iX 12/04, S. 70 (js)