Andreas Korthaus: "HTTP-Cache" für PHP-Applikation

Beitrag lesen

Hallo!

Also ich habe mir mal überlegt, wie ich mit dem Apachen User-spezifisches HTML-Output(durch PHP-Scripte erzeugt) cachen kann, ganz ohne überhaupt den PHP-Interpreter zu starten, da sich nur so ein wirklich deutlicher Caching-Effekt einstellt. Im Prinzip habe ich versucht, sowas wie PEAR::Cache per mod_rewrite zu implementieren. Das Problem dabei: woher kann der Apache wissen ob sich das HTML-Output eines PHP-Scriptes geändert hat?
Das versuche ich wie folgt zu erreichen: Ich verwende das Dateisystem, über das ich per RewriteCond testen kann, ob eine spezielle Cache-Datei existiert. Wenn ein Script welches gecached werden soll aufgerufen wird, speichere ich am Ende eine Datei im Dateisystem, im Verzeichnis /cache/.

$cache_dir = $_SERVER["DOCUMENT_ROOT"].'cache'.$_SERVER["SCRIPT_NAME"].'_'.$_SERVER["QUERY_STRING"].'/';
if(!is_dir($cache_dir))
  mkdir($cache_dir);

touch ($cache_dir.session_id());

Das erzeugt eine leere Datei mit dem Namen der SessionID des Benutzers, im angegeben Verzeichnis. Ich speichere auch den Query-String im Verzeichnis Namen, so dass ich auch hierdurch dynamische Seiten cachen kann. (das mkdir muss ggfs. etwas komplexer ausfallen bei tieferen Verzeichnisstrukturen)

Wenn jetzt Änderungen vorgenommen werden, die die Ausgabe dieses Scriptes betreffen, lösche ich alle Dateien in diesem Verzeichnis. (das ist auch die Archillesverse dieser Idee, ich darf nicht irgendwo vergessen dass dieses Script von einer Änderung betroffen sein könnte)

So, jetzt verwende ich folgene RewriteRule:

RewriteEngine on

RewriteCond %{REQUEST_URI}  !^/src/.*
RewriteCond %{REQUEST_METHOD} GET
RewriteCond %{HTTP:If-Modified-Since}  ^.+
RewriteCond %{HTTP_COOKIE} SID=([0-9a-z]{32}) [OR]
RewriteCond %{QUERY_STRING} SID=([0-9a-z]{32})
RewriteCond %{DOCUMENT_ROOT}/cache%{REQUEST_URI}_%{QUERY_STRING}/%1 -f

RewriteRule ^(.*)$      /         [R=304,L]

Dies funktioniert wie folgt:
Erst werden alle Verzeichnisse/Dateien ausgeschlossen, die sowieso nicht gecached werden sollen, in diesem Fall nur /src.
Dann wird nur die Request-Methode GET akzeptiert (POST macht wenig Sinn zu cachen)
dann gucke ich nach ob der Browser einen "If-Modified-Since" Header sendet, denn in diesem Fall kann ich davon ausgehen dass er eine Version im Cache hat.
Als nächstes versuche ich die SessionID aus Query-String oder Cache zu extrahieren.
Als letztes setze ich die ganzen Informationen zusammen, zum selben Pfad unter dem ich im PHP-Script eine Datei hätte erzeugen müssen.
Wenn unter diesem Pfad eine Datei existiert, weiß ich dass sich an dem Inhalt nichs geändert hat, und sende dem Browser einen 304-Header, "Not Modified". Dann wird der Browser die Version aus seinem Cache verwenden. So wird bei dem Request kein PHP-Interpreter-Durchlauf notwendig, es müssen nicht zig Klassen geladen und instanziert werden, nicht zig Datenbank-Abfragen...
Es sind eigentlich nur ein paar weniger Muster-Vergleiche und ein File-System-Check nötig.

Und das ganze funktioniert auch bisher hervorragend. Mich würde interessieren was Ihr von der Idee haltet, und ob Ihr vielleicht Ideen habt, wie man es besser machen kann.

Vor allem überlege ich noch, wie ich sicherstellen kann, dass der Cache auch wirklch bei jeder Änderung zurückgesetzt wird.

Mal als Beispiel ein News-Ticker. Wenn jemand eine Bestimmte News läd:
GET /news.php?id=123

dann wird das beim ersten Zugriff gecached, will heißen es wird die Datei

/cache/news.php_id=123/3487as76sd9830498098sa8d0923

angelegt. Beim nächsten Aufruf bekommt der User dann nur noch einen 304 Header, wenn eben diese Datei existiert. So, jetzt sagen wir mal der Redakteur ändert die News in der DB, mit einem ganz anderen Script, dann muss die oben genannte Datei gelöscht werden, genauer der gesamte Verzeichisinhalt, eben weil sich hierdurch die HTML-Ausgabe von /news.php?id=123 ändern wird -> die lokal gecachte Datei ist nicht mehr aktuell. Das ist ja soweit auch kein problem, nur muss ich da immer dran denken, das ist das Problem, z.B. bei weniger offensichtlichen Änderungen.

Sagen wir mal, der User kann seine Seite Personalisieren, z.B. das Datumsformat anpassen. Wenn er dieses bei seinen Einstellungen ändert, dann ändert sich auch das output von /news.php?id=123.

Das heißt, wenn der User sowas ändert, müssen alle Cache-Files, die das betrifft gelöscht werden. Gut, bei globalen Einstellungen würde ich je nachdem am besten alle Cache-Files des Users löschen.

ODer meint Ihr da würde ich mit einigen Clients Probleme bekommen? Ich könnte das ja auch abhängig vom USer-Agent machen. Ich denke über 90% der User-Agents könnten davon aber profitieren. Zur Not kann ich Caching noch in den User-Einstellungen komplett deaktivierbar machen.

Aber diese Probleme habe ich ja auch bei PHP-basierten Caches, die sind ja eigentlich auch nur darauf ausgelegt HTML-Output zu cachen, wenn sich von einer anderen Seite her was dran ändert, muss ich die Cache-ID auch entsprechend löschen.

Ich weiß, dass so eine user-spezifische Anwendung nicht gerade optimal zum cachen ist, aber meine Scrite erzeugen teilweise richtig viel Last, da sie oft sehr große HTML-Tabellen erzeugen, on-the-fly aus verschiedenen Datenbank-Tabellen zusammengefügt. Wenn die DB, dann richtig voll ist wird das sehr lahm.

Viele Grüße
Andreas