Asynchronizität mit Service Streaming
HTTP ist ein Request-Response basiertes Protokoll, basierend auf TCP/IP. HTTP 1.0 beschrieb dafür einen klaren Ablauf: mit jedem Request wird eine TCP/IP Connection zwischen zwischen Client und Server aufgebaut. An diese Connection bindet der Server einen Thread der den Response an den Client verschickt. Anschließend wird die TCP/IP Connection beendet und der Thread steht im Server wieder als freie Ressource zur Verfügung. Diese Semantik wird gemeinhin als thread per request bezeichnet.
Mit der Zeit wurde die Gestaltung der Webseiten anspruchsvoller, für ihre Darstellung mussten immer mehr Ressourcen (Bilder, Frames) nachgeladen werden. Das Aushandeln einer TCP/IP Connection hat aber einen gewissen Overhead. Für das Nachladen von Ressourcen sollte deshalb eine bestehende Connection einfach weiter verwendet werden. Mit dem keep alive Header in HTTP 1.1 können sich deshalb Client und Server darüber einigen, eine TCP/IP Connection für Wiederverwendung bestehen zu lassen. Zumindest mit Java hatte dies zur Folge, dass nun jede offen gehaltenen TCP/IP Connection ein JVM Thread belegt, was als thread per connection bezeichnet wird. Eine offen gehaltene TCP/IP Connection für sich belegt wenig Ressourcen und ist deshalb vollkommen akzeptabel. Solange mit einer solchen offen gehaltenen Connection Inhalte nachgeladen werden ist es auch akzeptabel, dass an diese TCP/IP Connection ein Thread gebunden ist. Problematisch ist eine offene TCP Connection, die nichts tut: sie blockiert den assoziierten JVM Thread.
Optimiert wurde dann in Java 1.4 mit der NIO API. Das Redesign zielte unter anderem darauf ab, dass das Warten auf Ereignissen an IO-Sockets eine Angelegenheit des Betriebssystems bleibt. JVM Threads müssen dann nicht mehr warten (blockieren): sie arbeiten lediglich tatsächlich eingetretene Ereignisse ab. Im Ergebnis lassen sich mit der gleichen Zahl Threads mehr Requests parallel abarbeiten. Mit der NIO API kann die JVM Ressourcen schonende thread per request Semantik kombiniert werden mit der Strategie, TCP/IP Connections offen zu halten.
Basierend auf diesem Entwicklungsstand (offene TCP/IP Connections ohne JVM Last) kann über Asynchronizität neu nachgedacht werden. Offenbar ist es nun technisch akzeptabel, einen Response deutlich verzögert oder unvollständig mit verzögerter Vervollständigung an den Client zu schicken. Werden solche verspäteten Nachrichten mittels geeigneter Clienttechniken in den richtigen Kontext gesetzt, ist das aus Clientsicht ein Server Push und damit die Grundlage für echte Asynchronizität. Für diese Technik setzt sich der Begriff service streaming gerade durch, sie ist die Grundlage für "reverse AJAX" Implementierungen. (Oft in Kombination mit dem "unsauberen" polling und passive piggypack.)
Die Sache hat jedoch serverseitig einen Hacken: für die Dauer der Verzögerung des Response ist ein Thread blockiert. Eine ähnliche Situation liegt vor, wenn die Abarbeitung eines Requests von langsamen, synchron gerufenen Services abhängt. Egal ob absichtlich verzögert oder durch schleppende Abarbeitung, es wäre sinnvoll den JVM Thread vorübergehend vom Request abkoppeln zu können. Dafür gibt es schon länger proprietäre Lösungen (zum Beispiel der CometProcessor im Tomcat, Glassfish (Grizzly) mit CometEngine und Continuation in Jetty 6) die service streaming im produktiven Umfeld überhaupt erst möglich machen. Im Rahmen des JSR 315 wurde dies nun mit "Asynchronous processing" standardisiert.
Asynchronous processing mit Servlet 3.0
Servlet Methoden lassen sich mit Annotationen (oder dem Deploymentdeskriptor) als asynchron markieren. Nach dem Aufruf von
final AsyncContext asyncContext = request.startAsync(ServletRequest req, ServletResponse res);wird der Request in einen asynchronen Zustand versetzt, was zwei wesentliche Konsequenzen hat. Zum einen wird der abarbeitende Thread vom Response abgekoppelt und beim Verlassen von service erfolgt KEIN commit. Zum anderen steht mit der AsyncContext Instanz nun Funktionalität zur nebenläufigen Abarbeitung zur Verfügung. Der Response wird abgschlossen, sobald complete an der AsyncContext Instanz gerufen wird (oder ein Fehler respektive Timeout eintritt). Insbesondere mit AsynchContext.start(Runnable) kann nun die eigentliche nebenläufige Abarbeitung angestoßen werden:
doGet(..) {
// Setzt die Abarbeitung in einen asynchronen Context
request.setAsyncTimeout(10000);
final AsyncContext asyncContext = request.startAsync();
asyncContext.start(new Runnable() {
public void run() {
// hier nebenläufige Abarbeitung mit der Hilfe
// des asyncContext-Objekts
:
asyncContext.commit();
}
});
// nach dieser Zeile erfolgt KEIN commit auf den Response!
}
Serverseitig ist damit gutes Skalierverhalten trotz asynchroner Verarbeitung gegeben: der Thread der Request-Abarbeitung steht dem Container sofort nach Start der nebenläufigen Verarbeitung wieder zur Verfügung. Es muss aber geprüft werden, ob die im Einsatz befindlichen Infrastrukturkomponente (Firewalls, Proxys und Reverse Proxys) derart langlaufende TCP/IP Connections überhaupt akzeptieren.
Literatur