Seite ausliefern in 11ms

Eine Webpage, die von einem WordPress-Monster generiert wird, hat in der Regel eine Auslieferungszeit, die sich in Sekunden messen lassen darf – zumindest, wenn die Komplexität der Webseite einen gewissen Schwellenwert überschritten hat.

Unser Webserver liefert die Rohdaten der Seiten (= HTML-Seite) inklusive Echtzeitüberarbeitung innerhalb von 11ms aus.

redis 11ms cache
redis 11ms cache

Redis – ich würde ja alles in dir speichern

Möglich macht das ein Cache, der als Datenbasis einen Redis-Server nutzt.

Daten, die in diesem Key-Value-Store abgelegt werden, verbleiben im Arbeitsspeicher. Das klingt jetzt für die Generation, die mit 640KB-Speichergrenzen aufgewachsen ist, gruselig.

Alles in den Arbeitsspeicher??? Sind wir hier bei den Flodders?

Ruhig Blut. Heute haben selbst Taschenuhren mehr Speicher und halbwegs vernünftige Betriebssysteme kennen weder eine 640KB-Barriere noch haben die das Bedürfnis den Speicher einfach ungenutzt in der Maschine versauern zu lassen.

Da fällt mir ein… mein erster Rechner war ein Apple ][. Der hatte 64KB Arbeitsspeicher und musste mit einer Integer- oder Real-Basic-Bootdiskette gestartet werden, je nachdem, was man so vorhatte. Zeigte 280×192 Pixel an (grün/schwarz) oder 40×48 bei 15(!!) Farben. Da hat man nix in den Speicher getan, was nicht von links und rechts und oben und unten genauestens geprüft und bewertet wurde.

Heute simma da nimma so. Wo ein ömmeliges Tetris sich 2000KB reinpfeifen darf, da können wir doch bitte ein paar Dutzend Megabyte für etwas sinnvolles investieren.

Beispielsweise Seitencache

Bei uns lungern im Redis aktuell so 1.400 generierte Endergebnisse der WordPress-PHP-Orgie herum, die nur darauf warten, dass ein Benutzer oder ein Bot die Infos abrufen möchte.

redis 1400
redis 1400

Das sind natürlich nicht einfache HTML-Ergebnisse ohne jede Optimierung.

Beim Erstellen unseres Caches, der üblicherweise Nachts „vorgewärmt“ wird, laufen durchaus ein paar Prozesse, um möglichst viel einzusparen, was sich auf Speicherverbrauch aber auch auf Performanceverhalten auf Benutzerseite auswirkt:

  • minifizieren und zusammenfassen sämtlicher JS und CSS Dateien, so weit das möglich ist
  • minifizieren des reinen HTML-Codes
  • Austausch der Medienlinks gegen CDN-Links
  • Vorwärmen des CDNs mit minifizierten JS/CSS-Dateien
  • weiteres…

Falls sich mal während der Wartung jemand auf unsere Seite verirrt (vor allem Bots, menschliche Nutzer schlafen Nachts üblicherweise), wird während der Wartung der Cache gedoppelt – und dann die Seite von dort aus ausgeliefert.

Wartung und Auslieferung gleichzeitig = Performanceinbruch?

Die Wartung ist im Prinzip ein sehr aufwändiges Ablaufen der gesamten Webseite. Also entsteht eine entsprechend hohe Last auf dem Server, da hier PHP und MySQL (und Redis und diverse andere Dinge) um die Wette rocken.

Aber das Abrufen der Webseiten durch einen Frontendbenutzer läuft in derselben Geschwindigkeit wie außerhalb der Wartung, da das Ausliefern einer vorgefertigten Redis-Seite nur minimalen PHP-Resourcenbedarf hat und minimale Serverlast erzeugt.

Wo gibt es das?

Nirgendwo. Selbst gebaut. In wochenlanger Arbeit. Ich berichte hier ab und zu von diesem Projekt, mir hat es viel Spaß gemacht (und tut es immer noch) um die beschränkten Resourcen herum eine Lösung zu bauen, die laut diversen Leistungsmessungen durchaus mit über 85% der sonstigen Webseiten mithalten kann.

Joar. So ein bisschen stolz drauf bin ich schon. Aber hei, bevor es anfängt zu stinken hör‘ ich lieber auf.

Ajax Backendanfragen cachen – unmöglich…?

Auf unserer Webseite benutzen wir viele Elemente, die dynamisch mit Content aus dem Backend versorgt werden.

Beispielsweise die Liste unserer Informationsartikel:

ya info
ya info

Die Elemente dieser Liste werden per Ajax aus dem Backend gelutscht – einzeln.

Requests ohne Ende

Also hustet so eine „Grid“-Übersicht mal eben ein paar Dutzend Backendanfragen in Richtung Webserver – und dann passiert das hier:

  • Webserver findet, dass admin-ajax.php eine PHP-Script ist und fährt erst mal eine Instanz des PHP-Interpreters hoch
  • Das Script wiederum möchte gerne das WordPress-Imperium im Zugriff haben, also wird die Maschinerie in Gang gesetzt und das WordPress-Backend macht sich breit (ein paar hundert PHP-Scripte werden geladen und Dutzende Datenbankabfragen losgeschickt – bevor irgendetwas tatsächlich passiert).
  • Dann verarbeitet admin-ajax.php netterweise auch mal den Ajax-Request bzw. leitet den an die zuständige Stelle weiter.
  • Diese fragt dann ihrerseits diverse Tabellen ab – über WordPressroutinen üblicherweise. Also wp_query und Konsorten, die allesamt nicht gerade den Ruf genießen hochperformant optimierten SQL anzuwenden – kann jeder bewundern, wenn er sein MySQL-Slowlog anschmeißt und sich dann mal per explain die dicksten Statements erklären lässt…
  • Wenn dann alle Beteiligten glauben, dass der Content so weit generiert ist, beendet sich das PHP-Konglomerat und stellt eine Antwort für den Ajax-Request bereit.
  • Der wiederum wird dann vom rufenden Javascript in die Seite gelötet.

Das stellen wir uns jetzt mal ein paar Dutzend vor und wir wissen, wie schön es ist, ein sich langsam inkarnierendes Postgrid zu bewundern – Ladezeiten im mehrstelligen Sekundenbereich sind die Folge.

Da möchte man doch mal – Cache?

Klassischer Anwendungsfall eines Caches ist, dass Daten, die sowieso gleich sind, nicht aus der langsamst möglichen Ecke geholt werden sondern von dort, wo es sonnig und warm ist; idealerweise aus dem Arbeitsspeicher, wenn es nicht anders geht, so zumindest aus der Tiefkühltruhe und nur noch schnell in die Micowelle (also servierfertig und nur dezent zu bearbeiten).

Angeblich geht das mit WordPress-Ajax nicht.

Selten so gekichert… natürlich geht das. Man muss nur die schulterlangen Gummihandschuhe anziehen, Nasenklammern drauf und dann in der Suppe wühlen, bis man die richtigen Stellen gefunden hat, an denen man was anflanschen muss.

Jetzt passiert das hier:

  • Ajax-Request schlägt im Backend auf.
  • admin-ajax.php wird immer noch durch eine neue PHP-Interpretinstanz interpre… ausgeführt.
  • ohne WordPress hochzufahren wird erkannt, dass die gewünschte Action mit dem passenden Schlüsselzeugs schon in der „Datenbank“ herumlungert und die Antwort umgehend an den Anfrager zurückgeworfen.

Dabei heißt „Datenbank“ hier ein Key im Redis-Inmemory-Store.

redis cache
redis cache

Aber einmal muss ich durch den Cache…

Die allererste Anfrage muss natürlich durch den gesamten Dschungel, sonst weiß der Redis ja nicht, wie die Abkürzung aussieht.

Das muss aber nicht zwingend mit dem ersten Besucher passieren, der sich auf die Webseite traut.

node.js precache
node.js precache

Das kann ein nächtliches Script machen, was bei der routinemäßigen Wartung mitläuft und die relevanten Ajax-Requests simuliert, die tagsüber so auf das Backend einschlagen. Dieses Script wiederum nutzt node.js und jsdom und diverse andere ähm „Hilfsmittel“ – darüber schreibe ich ein anderes Mal, mir ist schon schlecht 😉