Zwischenspeicherung mod_gzip-komprimierter Daten durch Proxy-Server

Komprimierung durch Verhandlung

Die Verwendung einer konfigurierbaren Komprimierungs-Funktion wie mod_gzip ist letzten Endes immer eine Art von Content-Negotiation, also eine bedingte Auslieferung unterschiedlicher Inhalte für denselben angeforderten URL, in Abhängigkeit von bestimmten Angaben des HTTP-Headers.

HTTP erlaubt allerdings die Zwischenspeicherung von Antworten auf HTTP-Requests in Caches, insbesondere bei Verwendung von Proxy-Servern. Wenn nun

  1. ein HTTP-Client eine Anforderung sendet,
  2. die entsprechende Antwort in komprimierter Form ausgeliefert und von einem Proxy gespeichert wird und
  3. anschließend ein anderer HTTP-Client eine Anforderung für denselben URL stellt,

dann hat der Proxy-Server - ohne im Besitz weiterer Informationen zu sein - ein Problem:

Denn nur der HTTP-Server kann (aufgrund seiner Konfiguration mit entsprechenden Filter-Regeln) letzten Endes herausfinden, ob auch der zweite HTTP-Client komprimierte Daten als Antwort erhalten darf.

Dies ist übrigens kein Effekt der Verwendung eines Komprimierungsverfahrens allein, sondern ein allgemeines Problem der Zwischenspeicherung von HTTP-Daten, deren Inhalt nicht eindeutig durch einen URL beschrieben sein kann, im Cache eines Proxy-Servers und ähnliche mit einem Gedächtnis ausgestattete Server auf dem Transportweg. Dies schließt Verhandlungsprozeduren aller Art ebenso mit ein wie die Übertragung zusätzlicher Informationen innerhalb der HTTP-Header, etwa Server-Authentification oder Cookies.

Performance-Anforderungen

Natürlich kann man versuchen, das Problem dadurch zu umgehen, indem man allen auf dem Weg zwischen Client und Server befindlichen Proxy-Servern explizit verbietet, die Daten der entsprechenden Antwort zu speichern (durch den Einsatz entsprechender HTTP-Header Expires: und Pragma: in HTTP/1.0 bzw. Cache-Control: in HTTP/1.1).

Aber der Zweck der Komprimierung ist es, die Übertragung der Daten (durch die Reduzierung der Datenmenge) zu beschleunigen - und die Zwischenspeicherung von Daten dient demselben Zweck (durch die Reduzierung der Zugriffe auf den HTTP-Server). Und es sollte nicht die eine Performance-Optimierung dazu führen, daß die andere nicht mehr eingesetzt werden kann, insbesondere da im vorliegenden Fall beide einander nicht ersetzen, sondern gegenseitig wirkungsvoll ergänzen können.

Informationen über Verhandlungs-Parameter

Die HTTP-Spezifikation enthält die Definition des HTTP-Headers Vary, wodurch der HTTP-Server den Proxy-Server darüber informieren kann,

Sein Wert kann eine Liste von Namen anderer HTTP-Header enthalten, deren Inhalt für die Auslieferung gerade dieser Antwort auf eine Anforderung entscheidend war. Der HTTP-Server kann dem Proxy-Server also sogar mitteilen, welche HTTP-Header die Entscheidung über den ausgelieferten Inhalt beeinflußt haben.

Wenn ein Proxy-Server eine Anforderung zu einem HTTP-Server weiterleitet und später die Antwort in seinem Cache speichern will, dann sollte er noch im Besitz der HTTP-Header der ursprünglichen Anforderung sein, wenn die Antwort des HTTP-Servers eintrifft.

Falls nun der HTTP-Server einen bedingten Inhalt einer Antwort durch den entsprechenden Vary-Header kennzeichnet,

Resultierende Einschränkungen für den HTTP-Server

Die vorherigen Ausführungen haben gezeigt, wie ein Proxy-Server die bedingte Auslieferung von HTTP-Antworten (die das Ergebnis einer Content-Negotiation darstellen) korrekt und gleichzeitig mit maximaler Ausnutzung seines Zwischenspeicher-Effekts behandeln kann - unter der Voraussetzung, daß

Letzteres bedeutet nun aber eine Einschränkung für die Freiheitsgrade des Verhandlungsvorgang. Denn wenn der Proxy-Server ausschließlich anhand von Informationen innerhalb einer HTTP-Anforderung entscheiden muß, ob er seinen Cache-Inhalt ausliefern darf oder nicht, dann dürfen sich die Verhandlungsregeln des HTTP-Servers ausschließlich auf Inhalte der HTTP-Header beziehen!

Diese Voraussetzung erfüllt mod_gzip nun aber leider nicht. Denn von den dort möglichen sechs Klassen von Filter-Regeln beziehen sich

Verwendet ein Server, der um mod_gzip erweitert wurde, also eine dieser 'illegalen' Filterregeln, dann kann der Proxy-Server nicht mehr in der Lage sein, korrekt über die Verwendbarkeit seines Cache-Inhalts zur Beantwortung weiterer Anforderungen zu entscheiden.

Dabei nützt es dem Proxy-Server auch wenig, wenn mod_gzip den Proxy-Server darauf aufmerksam machen würde, daß dieser offensichtlich überfordert sein wird (durch eine vollständige Liste der für diese Anforderung signifikanten Filterregelklassen innerhalb eines Vary:-Headers, falls dies denn erlaubt wäre). Alles, was der Proxy-Server tun könnte, wäre, das Auftreten einer der vier 'illegalen' Filterregelklassen als Kriterium zu verwenden, den Inhalt der Antwort nicht zwischenzuspeichern.

Das allein wäre nicht weiter schlimm - solange HTTP-Server sich darauf beschränkt, ausschließlich 'legale' Regeln zu verwenden, würde sein Server mit einem Proxy-Server optimal kooperieren können.

Leider ist jedoch genau dies mit mod_gzip 1.3.19.1a unmöglich.

Die Einbettung von mod_gzip 1.3.19.1a in die Architektur von Apache 1.3 erfolgt auf eine relativ komplexe Art und Weise:

Für die erfolgreiche Zulassung einer Anforderung zur Komprimierung ist mindestens die Erfüllung je einer include-Regel aus jeder der beiden Phasen erforderlich (und die Nicht-Erfüllung aller exclude-Regeln).

Da aber beide include-Regelklassen aus Phase 2 'illegal' sind, muß jede Liste der relevanten Filterklassen für eine erfolgreiche Komprimierung in der aktuellen Implementierung von mod_gzip mindestens eine 'illegale' Regelklasse umfassen.

Somit ist es unmöglich, einen Proxy-Server mit Informationen zu versorgen, welche dieser für die Entscheidung über die Nutzbarkeit eines Cache-Inhalts verwenden kann - die übermittelten Informationen werden das Verstädnis des Proxy-Servers immer übersteigen.

Verallgemeinerung auf beliebige HTTP-Content-Encodings

Dasselbe gilt letzten Endes für jeden HTTP-Header, der Element einer Verhandlung ist. Und die Kombination mehrerer HTTP-Header als Verhandlungs-Aspekte würde zu einem exponentiellen Wachstum der im Cache parallel zu haltenden Varianten führen.

Dennoch sprechen einige Argumente dafür, einen solchen Ansatz zu verfolgen:

Das genaue Verfahren ist keine Frage der Standardkonformität, sondern lediglich der maximalen Leistungsfähigkeit des Zwischenspeichers - hierbei sind sicherlich Konfigurationsmöglichkeiten vorzusehen und Erfahrungswerte über tatsächliche Anforderungen verhandelbarer Daten zu berücksichtigen.

Vary-Header in mod_gzip seit 1.3.19.2a

Beginnend mit der Version 1.3.19.2a sendet mod_gzip Vary:-Header - und zwar bei jeder Anforderung, bei deren Verarbeitung das Modul wenigstens einmal aktiviert wurde (unabhängig davon, ob dabei komprimierte Daten ausgeliefert wurden oder nicht).

Bei diesem Erkenntnisstand von mod_gzip ist jede Anforderung (unabhängig davon, ob die Antwort tatsächlich komprimiert ausgeliefert wurde oder nicht) potenziell eine Verhandlung:

mod_gzip kann derzeit noch nicht die optimalen Vary:-Header, nämlich deren minimal erforderlichen Umfang, erzeugen - dafür wäre es erforderlich, das Auswertungsverfahren für Filterregeln komplett umzuschreiben.

Als ersten Schritt sendet das Modul seit Version 1.3.19.2a einen Vary:-Header, der

enthält, denn jede dieser Regeln könnte den Ausschlag für das Ergebnis der Verhandlung geben, und in jedem dieser Fälle wäre das Ergebnis vom Inhalt des empfangenen HTTP-Headers abhängig. Dies kann natürlich in bestimmten Fällen viel zuviel sein (und dann die wirkungsvolle Zwischenspeicherung von Inhalten massiv behindern), aber es ist wenigstens mal ein Anfang.

Als Verbesserung dieser Vorgehensweise sendet mod_gzip 1.3.26.1a keinen Vary:-Header, falls die Komprimierung dieser Anforderung durch eine mod_gzip_item_exclude-Regel des Typs

verhindert wurde - denn die Auswertung dieser Regel kann nicht vom Inhalt des empfangenen HTTP-Headers abhängen, so daß in diesem Fällen tatsächlich gar keine Verhandlung (über veränderliche Ereignisse unterschiedlicher HTTP-Requests) statt gefunden hat.

Falls jedoch für bestimmte Dateien, von denen aufgrund anderer als dieser beiden Konfigurationsfälle fest steht, daß sie nie in komprimierter Form ausgeliefert werden, keine Vary:-Header gesendet werden sollen, ist es notwendig, für diese Dateien mod_gzip abzuschalten.

Ein Beispiel dafür, keine Vary:-Header für GIF-Bilder zu senden, die problemlos innerhalb eines Caches wie Squid 2.4 gespeichert werden dürfen, könnte so aussehen:

<FilesMatch \.gif$>
 mod_gzip_on No
</FilesMatch>

Für künftige Versionen bleiben derzeit folgende Aufgaben offen:

Verhandlungen über andere Aspekte als HTTP-Header

In ganz besonderen Fällen, nämlich beim Einsatz bestimmter Konfigurations-Direktiven, findet durch mod_gzip eine Verhandlung über Aspekte statt, welche gar nicht durch Namen von HTTP-Headern ausdrückbar sind. Hierzu zählen die Direktiven

In beiden Fällen kann mod_gzip einem Proxy nicht in Form von Namen von HTTP-Headern erklären, was es getan hat. Die gemäß der arrowHTTP/1.1-Spezifikation in diesem Falle angemessene Reaktion ist das Senden des HTTP-Headers Vary: *.

mod_gzip 1.3.26.1a sendet einen Vary: *-Header, falls die Direktive mod_gzip_min_http verwendet wurde.

Bezüglich der Direktive mod_gzip_handle_methods scheint derzeit noch nicht völlig klar zu sein, ob zwei HTTP-Requests nach derselben URI, aber unter Verwendung underschiedlicher HTTP-Methoden, tatsächlich dieselbe HTTP-Informationseinheit anfordern - dies wird darüber entscheiden, ob auch bei der Verwendung dieser Direktive ein Vary: *-Header gesendet werden muß. Mit dieses Problem wird sich eine kommende Version von mod_gzip befassen müssen.

Da ein Proxy-Server in diesem Falle aber die Art der durchgeführten Verhandlung nicht verstehen kann, ist er nicht befugt, Antworten mit dieser Angabe in einem Zwischenspeicher aufzubewahren.

Auf diese Weise verhindert die Verwendung einer dieser beiden Direktiven also vollständig die Zwischenspeicherung jeglicher von diesem HTTP-Server ausgelieferten Antworten, egal ob komprimiert oder nicht. Daher muß von der Verwendung dieser Direktiven inzwischen dringend abgeraten werden.

Der UserAgent als Sonderfall

Die parallele Speicherung von Varianten unterschiedlicher Verhandlungs-Parameter in einem Proxy-Cache mag sinnvoll sein, wenn nur wenige mögliche Werte tatsächlich auftreten können - so wie etwa im Falle von Content-Encoding. Sind jedoch sehr viele verschiedene Werte möglich, dann ist eine parallele Speicherung von Varianten nicht mehr praktikabel.

Genau dies trifft auf den UserAgent-Namen als Identifikation des HTTP-Client zu. Jede Sub-Version eines Browsers sendet einen komplexen UserAgent-String, welcher nicht nur Name und Version des Browsers, sondern auch weitere Informationen (Landessprache, Betriebssystem-Name und -Version etc.) enthält. Es gibt hunderte bekannter UserAgent-Strings - und dazu eine Reihe von Mechanismen, diesen UserAgent-String zu verändern. Manche Browser (wie Opera) erlauben dem Anwender sogar, selbst Einfluß auf den Inhalt dieses UserAgent-Strings zu nehmen, um sich für einen anderen Browser auszugeben (weil viele technisch inkompetente Gestalter dynamischer Webseiten ihr Angebot bedingt auf den Namen des Browsers aufbauen und manche Browser dabei unnötigerweise ausschließen, oder auch weil ihr Anwender im Interesse der Wahrung seiner Privatsphäre nicht unnötigerweise Informationen über die von ihm verwendete Computerausrüstung bekannt geben möchte).

Wie sinnvoll in manchen Fällen auch die bedingte Auslieferung komprimierter Webseiten auf die Identität eines HTTP-Clients sein mag (etwa angesichts der zahlreichen Bugs von Netscape 4), so spricht doch auf jeden Fall gegen die Verwendung des UserAgent-Strings als Grundlage einer HTTP-Verhandlung, daß der Inhalt dieses HTTP-Headers einerseits zu variabel ist, um zuverlässige Schlüsse daraus zu ziehen, und andererseits zu viele verschiedene Werte enthält, als daß ein Zwischenspeicher für all diese Verhandlungs-Varianten parallel die Ergebnisse von Anforderungen für denselben URL aufbewahren kann.

Seit Version 1.3.19.2a sendet mod_gzip einen Vary:-Header, welcher den HTTP-Header User-Agent: als Parameter der Verhandlung bezeichnet, falls eine entsprechende Direktive in der Konfiguration verwendet wurde. Aber die Wahrscheinlichkeit, daß bei einem Folgezugriff ein exakt übereinstimmender User-Agent:-Wert vorliegen wird (so daß dieser Client deshalb den gespeicherten Inhalt erhalten darf) ist sehr niedrig.

Zwar würde der HTTP-Server tatsächlich sogar große Mengen von UserAgents (die seiner Konfiguration gemäß als funktionell gleichwertig angesehen werden) während der Verhandlung identisch behandeln - aber der Vary:-Header erlaubt es dem HTTP-Server nicht, dem Zwischenspeicher mitzuteilen, welche Teilbereiche des UserAgent-Strings der HTTP-Server as signifikante Inhalte während der Verhandlung ausgewertet hat. Der Proxy-Server kann lediglich erfahren, daß der UserAgent irgend eine Rolle gespielt hat - und mit diesem Wissen muß der Proxy verschiedene UserAgents als unterschiedlich behandeln, auch wenn der HTTP-Server das vielleicht nicht tun würde.

Die Verwendung von Filterregeln mit Auswertung des UserAgent-Headers bewirkt also die vollständige Abschaltung der Zwischenspeicherung für derartig erzeugte Antwortpakete. Dieses Effekts solte sich ein Anwender von mod_gzip unbedingt bewußt sein - und deshalb nach Möglichkeit auf andere Filter-Mechanismen (mit einer geringeren Anzahl unterschiedlicher Werte) zurück greifen, um dieselbe inhaltliche Unterscheidung zwischen diesen HTTP-Clients zu bewirken.

(Michael Schröpl, 2004-02-02)