December 26, 2007
My-Hammer, das Fernsehen und die Serverlast: Teil 3
Was kann ich tun, damit meine Webseite nicht zusammenbricht, obwohl ein reichweitenstarker TV Sender einen längeren Beitrag über die Seite bringt? Teil 3 beschäftigt sich mit dem Thema Caching. Die anderen Teile:
Teil 1: Allgemeine Überlegungen
Teil 2: Datenbankoptimierung
Teil 2.1: Praxistipps Datenbank
Teil 3: Caching
Teil 4: Zukünftige Optimierungen (folgt)
Meiner Erfahrung nach kann man zusammenfassend sagen: Es gibt nur eine einzige Massnahme, die mehr Performance bringt als Caching, und das ist noch mehr Caching. Das gilt, um mal zum Haupthema zurückzukehren, vor allem in Bezug auf Performance bei plötzlichen Besucheranstürmen.
Statische Inhalte
Wenn man einen TV Beitrag über die eigene Plattform überleben will, dann gibt es nichts, aber auch wirklich gar nichts Wichtigeres als dies hier: Die Startseite der Plattform ist eine statische HTML Seite. Und zwar in aller Konsequenz, was heissen soll, dass der Aufruf der Seite nicht nur keine Datenbankverbindung zur Folge hat, sondern dass noch nicht einmal der PHP Interpreter auch nur gestartet wird. Die Startseite von My-Hammer ist eine .html Seite, die im Gegensatz zu den .php Seiten per Apache-Konfiguration mod_php noch nicht mal von Weitem zu sehen bekommt. Selbiges sollte konsequenterweise auch für alle JavaScript und CSS Dateien, die von der Startseite eingebunden werden, gelten. Ob man hierfür nun mit Proxylösungen arbeitet oder Seiten regelmäßig vorgeneriert, ist Geschmackssache.
Man darf nie den Performancevorteil reinen HTMLs unterschätzen - selbst wenn sich die Datenbanken schon alle verabschiedet haben und die Webserver bereits richtig unter Dampf sind: Eine HTML Seite auszuliefern schafft sogar ein Webserver, der schon ziemlich am Ende ist. Und man wahrt vor allem noch am ehesten sein Gesicht, wenn die ganzen neuen Benutzer, die aufgrund des TV Beitrages neugierig geworden sind, zumindest die Startseite zu sehen bekommen. Was immer man neben der Startseite noch an Seiten statisch vorgenerieren kann, ohne dass der angebotene Dienst selber “statisch” wird, sollte man natürlich machen (denn wie oft ändern sich schon Seiten wie Über uns?). Hierbei macht es Sinn, sich mithilfe der Zugriffsstatistiken einmal anzuschauen, welchen Weg neue Benutzer in der Regel auf der Plattform nehmen, um so auch wirklich jene Seiten zu cachen, die bei einem Ansturm am ehesten angesurft werden.
Eine dynamische Seite als statische Seite vorzugenerieren ist dabei natürlich die konsequenteste Version von Caching, aber nicht immer praktikabel. My-Hammer nutzt eine zweite Stufe des Cachings, bei dem zwar weiterhin dynamische Seiten ausgeliefert werden, diese aber ganz oder teilweise in dedizierten Cache-Backends (wir nutzen dazu memcached) abgelegt sind, um Ergebnisse teurer Datenbankabfragen, die nicht immer absolut live zur Verfügung stehen müssen, zwischenzuspeichern. Diese zwischengespeicherten Einträge können zum einen nach einer gewissen Zeit ablaufen und werden dann neu aus der Datenbank erzeugt, oder können gezielt als “dirty” markiert werden, wenn die Datenbestände die sie widerspiegeln sich ändern.
Wie oben erwähnt kann es sich außerordentlich lohnen, diese Cacheinhalte auf dedizierten Maschinen bereitzustellen - was wiederum deutlich zeigt, dass manche Massnahmen zur Performancesteigerung bestenfalls halbgar sind, wenn Admins und Programmierer nicht zusammenarbeiten.
Everybody needs a 304
(oder: Wie ich dem Browser des Users helfe, optimal zu cachen)
Bisher bin ich lediglich auf ein Ziel von Performanceoptimierung eingegangen - zu verhindern, dass die eigenen Server zusammenbrechen, wenn’s mal brenzlig wird. Man muss sich aber unbedingt bewusst machen, dass Optimierungen auf dem Server erstmal keinen Wert an sich darstellen, sondern nur dem eigentlichen Ziel dienen: dem User die Benutzung der eigenen Seite so schnell und angenehm wie möglich zu machen - indem die Seite grundsätzlich erreichbar bleibt, und indem die Seite sich so schnell wie möglich aufbaut.
Wenn man sich das erstmal bewusst gemacht hat, ist auch klar dass es sich sogar lohnen kann, etwas Rechenzeit auf dem Server zu investieren, um sie dem Client (also Browser) abzunehmen.
Aber der Reihe nach. Es gibt ein wichtiges Hilfsmittel, um den Aufbau einer Webseite im Browser deutlich zu beschleunigen (abgesehen von den üblichen Massnahmen wie geringer Dateigröße, möglichst wenig eingebetteten Objekten etc.), und das ist die Verwendung des HTTP Status 304 Not Modified. Diesen kann der Server senden, wenn er anhand der Anfrage des Clients erkennt, dass exakt der Inhalt, den der Browser bereits in seinem Cache hat, nochmal über die Leitung wandern würde - in diesem Fall sendet der Server diesen Inhalt dann eben nicht nochmal, sondern teil dem Browser nur mit, er möge auf den Inhalt seines Caches zurückgreifen.
Dies kann zu erheblichen Performancesteigerungen auf Seiten des Clients führen, denn die Zeit die zum Download des Inhalts einer Seite benötigt wird, entfällt.
Es gibt nun zwei Faktoren, die das Status 304 Handling beeinflussen und spezielle Anpassungen erfordern, um optimales Clientcaching zu ermöglichen: Die Auslieferung von Seiten über PHP Skripte (gilt prinzipiell auch für andere Skriptsprachen) und der Betrieb einer Plattform in einem Webserver-Cluster.
Zuerst zu letzterem: Um in der gegenseitigen Kommunikation festzustellen, ob ein Inhalt vom Server neu ausgeliefert werden muss oder der Browser den Inhalt aus dem eigenen Cache lädt, gibt es den sogenannten Etag. Ein ganz kurzer Abriss, wie die Verwendung abläuft. Der Client fragt eine Ressource beim Server an. Es ist der erste Zugriff innerhalb dieser Browsersitzung, deshalb schickt der Client kein Etag mit. Der Server sendet daraufhin die Inhalte aus, und schickt in den Headern den Etag des aktuellen Inhalts dieser Ressource mit, sagen wir “12345″ (der Server schickt dazu den Header Etag: “12345″).
Fragt der Client nun erneut dieselbe Ressource beim Server an, schickt er in seinen Headern wiederum die Information mit, dass er in seinem Cache bereits die Inhalte mit dem ETag “12345″ gespeichert hat, und der Server ihn informieren möge falls sich die Inhalte nicht geändert haben (der Client schickt dazu den Header If-None-Match: “12345″). Der Server kann dann schauen, ob die Inhalte die er ausliefern würde immer noch das ETag “12345″ haben, und in diesem Fall den erwähnten HTTP Status 304 senden, oder, falls Inhalt und ETag nicht mehr zueinander passen, den neuen Inhalt schicken.
Die Frage ist nun: Wie genau ist denn definiert, was im Etag steht? Nun, im Prinzip gar nicht. Es gibt kein vorgeschriebenes Format, wichtig ist nur die Definition des Etag an sich: dass nämlich ein eindeutiges Etag zu einem eindeutigen Inhalt einer bestimmten Ressource gehört, und deshalb festgestellt werden kann ob sich der Inhalt einer Ressource zwischen zwei Requests geändert hat oder nicht. Man kann sich den Etag deshalb der Einfachheit halber als Checksumme des Inhalts vorstellen (und in der Tat besteht eine Möglichkeit den Etag zu generieren darin, z.B. die MD5 Summe des Inhalts zu berechnen).
Woher kommt der Etag? Beim Apache ist es Teil der Kernfunktionalität, für eine angeforderte Ressource den Etag zu berechnen und mitzusenden, sowie entsprechend zu reagieren wenn ein Client den If-None-Match Header sendet. Alles out-of-the-box also, aber genau hier liegen für uns die Probleme:
Problem 1: Defaultmässig berechnet Apache den Etag für eine Ressource, indem eine Art Quersumme aus diesen Informationen generiert wird: I-Node-Nummer der angefragten Datei, letzter Änderungszeitpunkt (mtime) der angefragten Datei, und Größe der angefragten Datei. Betreibt man eine Webseite auf nur einem Server, hat man kein Problem, denn wenn z.B. die Datei /index.html zwischen zwei Aufrufen nicht verändert wird, hat sie bei beiden Zugriffen denselben Etag, da keiner der drei Faktoren inode, mtime, size zwischenzeitlich verändert wurde.
Betreibt man aber einen Cluster aus mehreren Webservern, und besteht die Möglichkeit, dass ein Client bei zwei aufeinanderfolgenden Aufrufen derselben Ressource zuerst auf einem Webserver, beim zweiten Aufruf aber auf einem anderen landet, dann ist, auch wenn auf beiden Servern die exakt gleiche Datei liegt, der Etag beide Male ein anderer, denn selbst wenn letzter Änderungszeitpunkt und Größe der Datei auf beiden Servern identisch sind: dass die I-Node-Nummer die gleiche ist, ist praktisch ausgeschlossen. Der Server wird also keine 304 Status senden, obwohl er es könnte.
Abhilfe ist zum Glück sehr einfach möglich, und lohnt sich schon beim Wechseln von einem auf zwei Server: Man muss dem Apache mitteilen, dass er die I-Node-Nummer nicht mehr zur Berechnung des Etag heranziehen soll. Dies erledigt an zentraler Stelle die Anweisung FileETag MTime Size. Mehr dazu im Apache Handbuch.
Problem 2: mod_php hebelt die Verwendung von Etag für PHP Skripte aus. Das macht ja prinzipiell auch Sinn: selbst wenn die Skriptdatei /index.php sich zwischen zwei Aufrufen inhaltlich überhaupt nicht geändert hat, kann sie dennoch völlig unterschiedliche Inhalte an den Client ausliefern - genau das ist ja Sinn und Zweck des Einsatzes von dynamischen Seiten.
Trotzdem kann es Sinn machen, dass der Server den Status 304 an einen Client sendet, wenn dieser dieselbe Ressource erneut anfragt. Zum Beispiel bei CSS Skripten, die von jeder Seite der Plattform eingebunden werden, und aus programmiertechnischen Erwägungen als PHP Skripte realisiert sind, aber deren Inhalt sich trotzdem sehr selten ändert. Jeder Seitenaufruf würde den Browser veranlassen, dies referenzierte CSS Datei anzufragen, und der Server würde jedes Mal den Inhalt senden, obwohl sich dieser seit dem letzten Aufruf nicht geändert hat. Das macht den Seitenaufbau im Client langsam, und ist zudem eine Ressourcenverschwendung.
Wie kann man nun sicherstellen, dass ein Client den 304 Status auch beim Abruf von PHP Skripten erhält, falls sich der Inhalt nicht verändert hat, aber auch auf keinen Fall einen 304 Status bekommt, falls der Inhalt sich geändert hat? Die Lösung ist leider nicht ganz so trivial wie beim ersten Problem, aber doch vergleichsweise einfach zu realisieren.
Da wie erwähnt der Zustand der Skriptdatei selbst praktisch keine Rolle spielt, darf man nur mit dem von diesem Skript auszuliefernden Inhalt arbeiten. Eine Lösung wäre, bei den Skripten, für die man den Etag Mechanismus einsetzen möchte, folgenden Code ans Ende anzuhängen (lässt sich natürlich einfach in eine zentrale Funktion kapseln):
<?php $output = ob_get_clean(); // Gesamte Ausgabe, die an den Client gesendet werden soll, abfangen und zwischenspeichern $etag = '"'.sha1($output).'"'; // Prüfsumme der Ausgabe berechnen // Ist der Inhalt identisch mit dem, den der Client gecached hat? if ($_SERVER['HTTP_IF_NONE_MATCH'] == $etag) // Wenn ja, dann sende nur den Status 304 { header('HTTP/1.x 304 Not Modified'); header('Etag: '.$etag); die(); } else // Wenn nicht, dann sende den Inhalt inkl. des neuen Etags { header('Etag: '.$etag); echo $output; die(); } ?>
Voraussetzung ist dafür die Verwendung von output buffering.
Eines muss man ganz klar festhalten - für den Server fällt exakt dieselbe Arbeit an, egal ob der User den Inhalt schlussendlich zugesendet bekommt oder nur die lapidare Meldung, er möge doch auf seinen Cache zurückgreifen. Es mag nach deutlich zuviel Overhead aussehen, PHP soviel Arbeit erledigen zu lassen, nur um das Ergebnis dieser Arbeit dann wegzuschmeissen; aber der Effekt auf die Lade- und damit Seitenaufbauzeiten beim Client ist wirklich beeindruckend, wenn man diesen Mechanismus geschickt einsetzt.
Filed by Manuel Kiessling at 12:19 pm under technology, software, php, My-Hammer, mysql, television
Trackback