Die Flinke Feder

HTTP-Wissen – früher wahr, heute falsch

Bernd Fondermann

Es gibt Punkte im Leben, da stellt man fest, dass bisher unumstößliche Wahrheiten plötzlich nicht mehr gelten. Zum Beispiel: „String-Konkatenation in Java mit dem Plus-Operator ist teuer. Nutze einen StringBuffer!“ Stimmte, bis Java-Compiler so schlau wurden, diese Optimierung selbst vorzunehmen. Und wenn, dann nutze man heute besser einen StringBuilder.

Ähnlich, aber schwieriger ist heutzutage die Lage beim Liebling aller Webanwendungen, dem HTTP-Protokoll. Zwar sind die Ports 80 und 443 immer noch gesetzt, jedoch geraten einige unverbrüchliche Dogmen langsam ins Wanken. Kurz noch mal zur Erinnerung, bevor man vielleicht für immer von ihnen Abschied nehmen muss:

  • (A) HTTP-Verbindungen sind kurzlebig!
  • (B) Jedem Request sein eigener Thread!
  • (C) Die Anzahl der gleichzeitigen Verbindungen zwischen demselben Browser und Server ist auf zwei begrenzt!

Was ist eigentlich passiert? Es fing wohl an, als Googles GMail rauskam. Eine Seite, deren Inhalt sich aktualisierte, im Hintergrund, ohne einen kompletten Reload durchzuführen. Damals neu, heute Standard. Das Stichwort lautet AJAX, also asynchrone HTTP-Requests. Asynchron bedeutet in diesem Fall, dass JavaScript im Browser einen neuen HTTP-Request initiiert, aber nicht blockiert, bis das Ergebnis da ist, und dann auch nicht den DOM komplett neu initialisiert, sondern mit der Programmausführung fortfährt. Stattdessen wird, sobald das Ergebnis vom Server eingetroffen ist, ein JavaScript-Callback aufgerufen. Webseiten, die Ajax nutzen, empfindet der Nutzer als nahtlosere Oberflächen, die deswegen immer beliebter werden. Außerdem gehen nur Daten-Deltas über das Netz, es müssen nicht alle Teile einer Webseite neu auf dem Server generiert und zum Client übertragen werden. Damit lassen sich theoretisch Server-CPU-Zyklen sparen, denn auch die Verarbeitungszeit auf dem Server bindet kostbare Resourcen.

Apropos Server: Viele Webserver warten einen nicht unerheblichen Teil der Zeit beim Beantworten eines HTTP-Requests auf andere Server, die Daten zuliefern müssen, sowie darauf, dass die Antwort dann auch häppchenweise über die Leitung zum Client übertragen wird. Und in der alten Welt ist der laufende Request fest einem Worker Thread zugewiesen. Der muss dann warten und kann somit nicht für andere HTTP-Anfragen genutzt werden. So funktionieren zum Beispiel die altgedienten Apache httpd und Apache Tomcat. Theoretisch ließe sich der Durchsatz des Webservers wesentlich erhöhen, wenn Threads nicht blockieren würden, während sie auf Dritte warten. Dann könnte in dieser Zeit ein anderer Request zum Zuge kommen. Jedoch würde das oftmals nur dazu führen, dass der Druck auf die Backend-Systeme erhöht würde. Ein höherer Durchsatz ergibt sich dann nicht.

Heute verhält sich der Client oft so: Da der Browser nicht weiß, wann neue Daten auf dem Server abzuholen sind, muss er regelmäßig dort anfragen. Dabei gibt es zwei Strategien: Regelmäßiges „Pull“, alle Clients fragen also beim Server nach, zum Beispiel einmal pro Sekunde. Das führt zu einem erhöhten Aufkommen an Requests, und darunter sind möglicherweise viele, für die die Antwort lediglich lautet: „Nix Neues auf dem Server!“ Die andere Strategie nennt sich „Long Polling“: Es wird „auf Verdacht“ eine Anfrage gestartet, aber so lange vom Server hingehalten, bis es entweder einen Timeout gibt (dann wird sofort vom Client eine neuer Long-Polling-Call initiiert) oder der Server etwas Neues mitzuteilen hat und damit eine Response füllt („Push“) und den laufenden Request nutzt. Mit „Long Polling“ ist die alte Regel (A) schon mal außer Kraft gesetzt. Mit dem Aufkommen von Websockets, was eine langlebige Full-Duplex-Verbindung zwischen Browser und Server möglich macht, wird sie endgültig nicht mehr gelten.

Aufgrund der Bindung eines Threads an den Request setzt Long Polling dem Server besonders zu und das Parken des Request wird ohne Seiteneffekt möglich. Doch wie lässt sich das umsetzen?

Die Antwort liefert ein Architekturmuster, das mehr Beachtung verdient hätte, nämlich die so genannte Staged Event-driven Architecture (SEDA). Im Kontext eines Webservers muss man sich das wie folgt vorstellen: Ein HTTP-Request wird vom Server entgegengenommen und innerhalb eines HTTP-Worker-Threads eingelesen. Dieser Thread ist aber nur so lange an den Request gebunden, wie Daten zur Verarbeitung anliegen (gilt sowohl für Daten vom als auch zum Client). Aus dem vollständigen eingelesenen Request generiert er einen HTTP-Request-Event, den er – statt ihn selbst zu verarbeiten – an die nächste interne Verarbeitungsstufe (Stage) weiterreicht. Diese generiert beispielsweise einen weiteren Event zur Datenbankabfrage, der an die Datenbank-Stage weitergereicht wird. Während die Datenbank das Ereignis vorbereitet, widmet sich der Staging Thread schon wieder anderen Dingen. Antwortet die Datenbank schließlich, so wird ein anderer Thread über einen asynchronen Callback sie verarbeiten und wiederum an die aufrufende Stage asynchron zurückliefern. Das führt nicht nur zu einer logischen Entkoppelung der Schichten, sondern auch zu einem entkoppelten Ablauf der verschiedenen Stages zueinander. Am Ende der Kaskade steht wieder die HTTP-Worker-Stage, die eine Response generiert und an den anfragenden, brav wartenden HTTP-Socket ausliefert.

Webserver, die so aufgebaut sind, sind in der Lage, viel mehr HTTP-Verbindungen parallel offen zu halten und scheinbar gleichzeitig zu bedienen. Die Last wird voll an das Backend weitergegeben, das entsprechend skalieren muss.

Apache MINA stellt eine solche SEDA-Infrastruktur für die Java-Welt unter Ausnutzung von Java NIO bereit. Für Details sei auf die Beispiele verwiesen. Besonders profitiert haben davon seit eh und je Internetprotokolle, die mit langlaufenden persistenten Socket-Verbindungen arbeiten und über die u. U. nur sporadisch Daten fließen, wie FTP, SSH und XMPP. Nicht umsonst sind die entsprechenden Apache-Projekte FTPServer, SSHD und Vysper unter dem MINA-Mantel angesiedelt.

Für HTTP wird SEDA jetzt auch immer interessanter. Ein einfacher Anwendungsfall ist der Reverse Proxy. Ein Webserver nimmt allerlei HTTP-Requests entgegen und verteilt sie ausschließlich auf die Worker-Webserver-Farm im Hintergrund. Das Staging übernimmt also gleich eine ganze Gruppe von Servern.

MINAs Asyncweb implementiert prototypisch einen HTTP-Server mittels MINA und NIO. Das Projekt wird allerdings derzeit nicht weitergepflegt. Auch Apache Tomcat bietet mittlerweile Unterstützung für NIO. Somit wird auch Annahme (B) immer brüchiger. Weiß man nicht mit Sicherheit, welche Architektur zum Zuge kommt – blocking oder non-blocking – ist übrigens auch Vorsicht bei der Nutzung von ThreadLocals angesagt!

Bleibt noch Regel (C). Die HTTP-Spezifikation RFC 2616 empfiehlt in Abschnitt 8.1.4 die Anzahl der gleichzeitig erlaubten Verbindungen zwischen einem Browser-Server-Paar auf zwei zu beschränken. Doch neuere Browserversionen halten sich daran nicht mehr. Je nach Browser sind bis zu acht Verbindungen möglich. Macht jeder Nutzer acht statt zwei Verbindungen auf, schränkt das die Zahl der gleichzeitig zu bedienenden Nutzer auf ein Viertel ein.

Fazit ist, dass sich mit den Anwendungen auch die serverseitigen Architekturen ändern müssen. Das hat aber zur Konsequenz, dass einige alte Regeln womöglich nicht mehr gelten. Auf andere Bereiche der HTTP-Weisheit, in denen sich das Wissen der Alten überholt haben könnte, wie Firewall-Timeouts und TCP/IP-Finetuning, möchte ich an dieser Stelle nicht weiter eingehen. Seien Sie bitte nicht überrascht.

Bernd Fondermann (bernd[at]zillion-one.com) ist freiberuflicher Softwarearchitekt und Consultant in Frankfurt a. M. und Member der Apache Software Foundation. Er beschäftigt sich mit innovativen Open-Source-Technologien wie Apache Hadoop oder Lucene und bietet unter zillion-one.com einen Big-Data-Hosting-Service an.
Geschrieben von
Bernd Fondermann
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.