Michael Schröpl: Predigt: KeepAlive und Transport-Encodings

Beitrag lesen

Hi Cyx23,

Der Selfserver hat wohl eine KeepAliveTimeout von 15 Sekunden, d.h. das
unterschiedliche Verhalten wäre damit nicht erklärbar.
Was aber bedeutet "Die Zeitspanne um auf die nächste Abfrage desselben
Clients mit derselben Verbindung zu warten." konkret?

(ich bewege mich mal auf dünnem Eis ... Netzwerk-Experten, bitte korrigiert mich, falls erforderlich)

Der Aufbau einer HTTP-Verbindung setzt den Aufbau einer TCP/IP-Verbindung voraus, erfordert also ein explizites Handshaking mit dem Server, um eine entsprechende Übertragung vorzubereiten (der Server will ja nicht den Port 80 zunageln, wo er ständig neue Requests annehmen möchte).
Client und Server einigen sich also auf einen zu verwendenden Port, und über diesen wird dann der eigentliche HTTP-Request gesendet und die Response zurück übertragen. Anschließend kann die Verbindung geschlossen und dieser Port wieder freigegeben werden.

HTML-Seiten haben aber eine gewisse Charakteristik. In vielen Fällen enthalten HTML-Seiten Referenzen auf weitere HTTP-Ressourcen: Bilder, JavaScript- und CSS-Dateien, Applets, whatever. KeepAlive ist nun eine Erweiterung zu HTTP, welche es dem Server erlaubt, die Verbindung nach der Übertragung der Antwort nicht sofort zu schließen, sondern dies von einer komplexeren Logik abhängig zu machen. Und solange die Verbindung besteht, _darf_ der Client eben weitere Requests senden, ohne dafür den entsprechenden Handshake durchführen zu müssen. Die konkreten 15 Sekunden sind dabei ein "sophisticated guess", den man am besten der Charakteristik der eigenen Webseiten anpassen sollte, wenn man entsprechende Werte ermitteln könnte ... wobei allerdings nicht nur die Zahl und Größe der referenzierten Ressourcen eine Rolle spielen, sondern beispielsweise auch noch das Caching-Verhalten bezüglich dieser Ressourcen (welches wiederum von weiteren eigenen Aktionen abhängt).
KeepAlive lebt also von dem Charakter heutiger real existierender HTML-Seiten, der in der Frühphase der HTTP-Spezifikation vielleicht anders gedacht war (weniger modular und weniger multimedial?).

Konkret beim Apache kann man zwei Obergrenzen einstellen: 1. eine Zeitdauer und 2. eine Anzahl von Requests. Wird eines von beiden erreicht, dann klappt der Server die Verbindung eben doch zu. Der Client kann selbst jederzeit aufgeben (und tut dies auch immer dann, wenn er KeepAlive einfach nicht unterstützen kann); er verliert dabei schlimmstenfalls Performance. Der Server verhindert durch das Schließen der Verbindungen, daß ein Angreifer den KeepAlive-Mechanismus für eine DoS-Attacke nutzen kann.

Die Grenzen der Benutzbarkeit von KeepAlive sind allerdings vielfältig. Insbesondere müssen _sämtliche_ HTTP-Zwischenstufen auf der gesamten Verbindung diesen Mechanismus unterstützen, sonst nützt er gar nichts. Schon wenn Du über einen einzigen Proxy-Server drüber mußt, der das nicht kann (und die Verbindung, deren Stabilität der Server "garantiert hat", eben doch unterbricht), kann dir das passieren, was Du gerade erlebst: Der Browser versucht, eine Verbindung zu verwenden, die schon nicht mehr existiert, weil der Server ihn dazu ermutigt hat - und keiner von beiden erkannt hat, daß zwischen ihnen ein "man in the middle" mitspielt, der sich nicht an die zwischen _ihnen_ ausgehandelten Regeln hält (weil er dazu nicht "genug HTTP" versteht).

Im konkreten Falle ist KeepAlive überhaupt eine nicht ganz ungefährliche Idee. Sie unterwandert nämlich in gewisser Weise das Konzept von HTTP, und wenn man nicht weiß, was man tut, kann man sich existierende Mechanismen damit lahmlegen.
"Normalerweise" enthält ein HTTP-Header für eine anschließend auszuliefernde Ressource insbesondere eine "Content-length:"-Angabe. Bei der Auslieferung des Inhalts statischer Dateien ist es auch einfach, diesen Header zuerst zu berechnen und anschließend den Datei-Inhalt auf die Leitung zu kippen. Was aber ist, wenn der Inhalt dynamisch berechnet wird - sagen wir mal, von einer Suchmaschine oder einem Datenbankzugriff? Die entsprechenden Applikationen wissen nicht, daß das HTTP-Frontend gerne die Gesamtgröße der Ausgabe wissen möchte - sie liefern einfach Daten. Nun müßte also das HTTP-Frontend sämtliche Daten absaugen, das HTML-Dokument generieren und puffern, nun dessen Größe berechnen und schließlich zuerst den HTTP-Header und danach das Dokument ausliefern. Was die Antwortgeschwindigkeit nicht gerade positiv beeinflussen würde.
Weil dies so ist, kann HTTP noch etwas anderes: Es kann "chunks" als Transport-Codierung verwenden (http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1). Ein dynamisches serverseitiges Skript kann also einfach seine Ergebnisse ausgeben, ohne sich um die Content-length zu kümmern; am Ende darf ein Trailer fehlende HTTP-Header nachliefern. Der Client, welcher diese Transport-Codierung versteht, muß alles puffern und wieder so zusammenbauen, als wäre es in einem Rutsch und in der richtigen Reihenfolge eingetroffen. Solange der Trailer auch zuverlässig kommt, ist das noch kein Problem.
Nun ist der Trailer aber leider optional! Der Server kann einfach aufhören zu senden, und der Client muß damit fertig werden! (Sonst unterstützt er HTTP/1.1 nicht vollständig.) Solange der Server am Ende seiner Übertragung brav die Verbindung kappt, wie das in HTTP vorgesehen ist, liegt immer noch kein Problem vor.
Aber jetzt übertrage mal mit chunked transport encoding über eine HTTP-Verbindung mit KeepAlive ... woher weiß der Client jetzt, daß er wieder senden darf, wenn die empfangene HTTP-Response weder eine Längenangabe noch eine "Klammer zu" enthält? Das würde genau Deine 15 Sekunden Wartezeit erklären. Und der Apache setzt "chunked" encoding tatsächlich ein - sowohl bei CGI- als auch bei SSI-URLs ... (vermutlich auch bei PHP, denke ich mal.) Mein HTTP-Trace-Skript kann das nicht visualisieren, weil es ein Perl-Modul (LWP) einbindet, welches dies offenbar kapselt; Christian Kruses Version desselben Skript (das direkt socket-Verarbeitung macht) kann beispielsweise "chunked" responses derzeit gar nicht verarbeiten.

So ist das eben mit den Standards: Wenn alle sich daran halten würden, würde vieles prima funktionieren ... das ist der Grund, weshalb man KeepAlive sowohl im Apache als auch in Mozilla ein- und ausschalten kann: Man muß wissen, was man tut, und ob das eigene Szenario den Einsatz sinnvollerweise ermöglicht oder nicht.
Und würden sich die Proxies auf dem Weg daran halten, "Via:"-Header zu senden, in denen drin stünde, was sie können und was nicht, dann könnte der Client im vorliegenden Falle sogar gewarnt werden, dem Keep-Alive-Versprechen des HTTP-Servers besser nicht zu glauben, weil der Proxy sich nicht daran gebunden fühlt. Aber natürlich ist dieser Header auch wieder nur optional ... und wenn die Beteiligten nicht miteinander reden, wie sollen sie einander dann verstehen und sinnvoll zusammenarbeiten können?

Ein ganz ähnliches Problem ist beispielsweise das Cachen von HTTP-Negotiation-Ergebnissen in Proxy-Servern: Auch hier kann der "Mittelmann" heftig versagen, wenn er nicht weiß, was er tut - nicht zuletzt dann, wenn der Server nicht daran gedacht hat, ihn mit entsprechenden Zusatzinformationen zu versorgen, die _nur_ dem Proxy etwas nützen und für den Browser entbehrlich gewesen wären ... das ist der Grund für die Existenz der aktuellsten mod_gzip-Version.

Viele Grüße
      Michael

--
T'Pol: I apologize if I acted inappropriately.
V'Lar: Not at all. In fact, your bluntness made me reconsider some of my positions. Much as it has now.
(sh:| fo:} ch:] rl:( br:^ n4:( ie:% mo:) va:| de:/ zu:| fl:( ss:) ls:~ js:|)
Auch diese Signatur wird an korrekt konfigurierte Browser gzip-komprimiert übertragen.