Author: Michael Schröpl
Description: Eine Diskussion der Probleme eines Proxy-Servers bei der korrekten Verarbeitung während der Zwischenspeicherung komprimierter HTTP-Daten sowie der erforderlichen Kooperation durch den HTTP-Server
Keywords: Apache, HTTP, Kodierung, gzip, Komprimierung, Proxy, Cache

# 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:

- Darf er den gespeicherten Inhalt auch an diesen zweiten HTTP-Client senden, oder
- muß er die Anforderung an den HTTP-Server weiterleiten?

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,

- ob eine Antwort das eindeutige Ergebnis der Anforderung eines URL war oder
- ob andere Eigenschaften einer Anforderung desselben URLs zu abweichenden Ergebnissen führen können.

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,

- muß der Proxy-Server nicht nur den Inhalt dieser Antwort, sondern auch alle Informationen der relevanten HTTP-Header (deren Namen in der Werteliste des HTTP-Headers `Vary` der Antwort aufgeführt sind) aus der Anforderung in seinem Cache abspeichern, und
- er darf diesen gespeicherten Inhalt nur dann als Antwort auf weitere Anforderungen ausliefern, falls die Informationen der entsprechenden HTTP-Header einer solchen weiteren Anforderung zu denen der ursprünglichen Anforderung 'paßt', d. h. für jeden einzelnen dieser Header semantisch mit dem Wert des Original-Requests übereinstimmt.

## 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ß

- der HTTP-Server den Proxy-Server mit ausreichenden Informationen über die Parameter der Verhandlung versorgt und
- daß dem Proxy-Server die entsprechenden Informationen bei einer nachfolgenden Anforderung desselben URL durch einen weiteren HTTP-Client zur Verfügung stehen können.

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

- zwei 'legale' (`reqheader` und `uri`) ausschließlich auf Inhalte des HTTP-Headers, aber
- vier andere 'illegale' (`rspheader`, `handler`, `file` und `mime`) auf Informationen, welche erst während der Auswertung der Anforderung durch den HTTP-Server verfügbar werden.

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:

- In Phase 1 der Verarbeitung prüft `mod_gzip`, ob es sich überhaupt für die Verarbeitung des Ergebnisses dieser Anforderung interessieren und entsprechende Vorbereitungen dafür treffen sollte - aufgrund der Regeln von vier Klassen (`reqheader`, `uri`, `file` und `handler`, d. h. zwei 'legale' und zwei 'illegale' Regelklasse).
- In Phase 2 der Verarbeitung prüft `mod_gzip`, ob es nun tatsächlich den (inzwischen vorliegenden) Inhalt der Antwort komprimieren soll - aufgrund der Regeln von zwei Klassen (`rspheader` und `mime`, beides 'illegale' Regelklassen).

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:

- Die Funktion eines Caching Proxy ist es, die Ressource 'Bandbreite' durch den Einsatz der Ressource 'Speicherkapazität' zu kompensieren. Es darf also vorausgesetzt werden, daß ein solcher Zwischenspeicher mit einem hohen Maß an Speicherkapazität ausgestattet ist, um bei bestimmten Dokumenten auch eine große Anzahl von Varianten aufnehmen zu können.
- Es ist zu vermuten, daß nur ein geringer Anteil der Menge aller Dokumente das Ergebnis einer Verhandlung zwischen Server und Client sein werden. Somit würde der Speicherbedarf für eine flexiblere Handhabung von Varianten innerhalb des Zwischenspeichers nicht direkt proportional zum Gesamtvolumen der gespeicherten Dokumente wachsen.
- Falls trotz dieser Einschränkungen eine Darstellung sämtlicher Permutationen von Verhandlungs-Attributen die Kapazität eines Zwischenspeichers übersteigen würde, besteht immer noch die Möglichkeit, dies durch etablierte Algorithmen (Verdrängung der am längsten nicht nachgefragten Varianten) oder auch durch die Definition von Prioritäten zu steuern.
- Zudem ist das Betriebsziel eines Caching Proxy die Beschleunigung der Übertragung von Daten. Wenn ein solcher Zwischenspeicher die Möglichkeit erhält, mit einem anderen Verfahren zur Beschleunigung der Datenübertragung zu kooperieren, dann sollte er daran interessiert sein, dies zu tun. Dies könnte bedeuten, daß das Content-Encoding gegenüber anderen Verhandlungs-Aspekten gespeicherter Daten bevorzugt werden sollte.

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) potentiell eine Verhandlung:

- mindestens über den HTTP-Header `Accept-Encoding` und
- möglicherweise auch über weitere HTTP-Header (nämlich all diejenigen, welche innerhalb von Filterregeln der Klasse `reqheader` auftreten)

`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

- die Angabe `Accept-Encoding` sowie
- die Namen sämtlicher innerhalb beliebiger `reqheader`-Regeln verwendeten Header

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

- `file`
- `uri` bzw.
- `handler`

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:

- Die vollständige Erkennung, daß die Reaktion auf die aktuelle Anforderung niemals zur Auslieferung komprimierter Daten führen kann, weil eine `mod_gzip_item_exclude`-Regel feuert, die nicht von einer Eigenschaft eines Request abhängt.
- Die Erkennung, daß eine Verhandlung statt gefunden hat, die nicht durch eine Liste von HTTP-Header-Namen beschrieben werden kann - in diesem Fall sollte `Vary: *` gesendet werden (und die Dokumentation von `mod_gzip` explizit darauf hinweisen, diese Direktiven nur dann zu verwenden, wenn es unbedingt notwendig erscheint, da ihre Gebrauch die Wirkung der Arbeit von Caching Proxies beeinträchtigen wird).
- Die Prüfung, ob Konstellationen möglich sind, in denen nur eine Teilmenge der Header-Namen sämtlicher `reqheader`-Regeln im `Vary:`-Header benötigt werden - je weniger Namen dort angegeben sind, desto weniger Varianten müssen im Proxy-Cache parallel gespeichert werden.

## 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

- `mod_gzip_min_http` (minimal erforderliche HTTP-Version) sowie
- `mod_gzip_handle_methods` (zu verarbeitende HTTP-Methoden)

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 HTTP/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, 2002-09-30)