Barcamp Stuttgart 4: Ressourcensparen bei Websites - Ladezeit
Jeder kennt das Problem einer sich gemütlich aufbauenden Website. Die Ursachen für dieses Verhalten können vielfältig sein. Zum einen kann die Verbindung zum Webserver teilweise gestört sein und zum anderen können auf der Server- und/oder Clientseite "teure" Prozesse im Gange sein, welche die Inhaltsdarstellung verzögern.
Am zweiten Tag vom Barcamp Stuttgart 4 habe ich mich dazu entschlossen, eine Session zum Thema Ressourcenverbrauch und deren Auswirkungen auf die Ladezeit von Websites zu halten. Der Phrase "Besser spät als nie" folgend, ist hier nun endlich meine Zusammenfassung dieser Session.
Ablauf eines Webseitenabrufs
Der Abruf einer Webseite von einem Webserver erfolgt in einer Vielzahl von Einzelschritten auf Client- und Serverseite. Stark vereinfacht betrachtet geschieht dieser Vorgang in drei Schritten.
Anfrage
Ungefragt liefert kein Webserver etwas aus, da laut HTTP stets der Client die Initiative beim Abruf ergreifen muss. Weiter ist der Client dem Webserver nur während der Anfragenbearbeitung bekannt. Ist eine Anfrage einmal beantwortet, muss der Client bei einer Folgeanfrage unter Umständen gewisse Zustandsinformationen mitliefern, um identifiziert und korrekt behandelt werden zu können. Beispielsweise wird nach einem Loginvorgang an einer Webapplikation vom Client ein Cookie bei jeder Folgeanfrage mitgeliefert, um jede einzelne Benutzersitzung von der Applikation verfolgbar zu machen.
Antwort
Der Webserver nimmt die Anfrage entgegen, berechnet die Antwort und liefert sie zurück an den Client. Dabei können je nach Webserver und evtl. vorliegender Webapplikation mehrere Bearbeitungsschritte zur Beantwortung der Anfrage erforderlich sein. Hier sind ein paar Varianten, wie sowas aussehen kann.
Spezialisierung
Ein Webserver, welcher sämtliche auslieferbaren Inhalte bereits im Hauptspeicher vorhält und direkt versenden kann. Bei steigender Komplexität der Inhalte, steigt der Haupspeicherverbrauch ebenfalls an. Dieser Ansatz wird meist nur bei spezialisierten Webservern eingesetzt, welche auf einige wenige Anwendungsfälle optimiert wurden. Als bestes Beispiel sehe ich dabei opentracker.
Dateiauslieferung
Ein Webserver, welcher Inhalte von Dateien aus einem relativ zügig zugreifbaren Dateisystem direkt ausliefert. Der Hauptspeicherverbrauch für den Webserver selbst kann dabei minimal ausfallen. Bei jeder Anfrage eines Clients, bestellt sich der Webserver vom Betriebssystem, in dem es läuft, die jeweilig angefragte Datei und liefert deren Inhalte aus. Nicht jeder Zugriff auf das Dateisystem muss dabei zwangsläufig in einem Festplattenzugriff münden. Geschickte Betriebssysteme sind in der Lage, die Dateizugriffe zu cachen. Finden lediglich lesende Zugriffe statt, ist der Cache nach dem Start des Webserver und einigen Clientanfragen warmgelaufen und lässt den Webserver fast so geschwind reagieren, wie die spezialisierte Variante mit reiner Hauptspeicherdatenhaltung. Die Größe des Caches hängt von der Größe des verfügbaren Hauptspeichers und der Konfiguration des Betriebssystems ab. Meine Website wird beispielsweise exakt nach diesem Prinzip von gatling ausgeliefert.
Rendern
Besonders bei äußerst dynamischen Inhalten wird meist auf einen Webserver zurückgegriffen, welcher nach dem Empfang einer Anfrage beginnt, die Antwort erst zusammenzustellen und sie danach auszuliefern. In der Regel erfolgt dieser Zusammenstellungsprozess über ein Programm, welches verschiedene Inhaltselemente von einem Datenbankserver bezieht. Der Vorteil bei diesem Ansatz ist das geordnete Ablegen der Inhalte und derer Beziehungen zueinander in der Datenbank. Der Nachteil ist die Programmausführungszeit, welche mit steigender Komplexität der Inhalte ebenfalls ansteigen kann.
Darstellung
Der Client empfängt die Antwort, stellt die Webseiteninhalte dar und lädt bei Bedarf weitere ihm fehlende eingebundene Inhalte mittels weiterer Abfragen nach. Je nach Art des Clients ist dies sehr unterschiedlich. Programme wie wget oder httrack speichern die empfangenen Daten im Dateisystem. Browser stellen die Inhalte hingegen direkt auf dem Bildschirm dar.
Zeit sparen und Ressourcen kürzen
In allen drei Schritten wird Zeit für die Durchführung der Abfrage verbraucht. Besonders wenn Webseiten mit einem Browser abgerufen werden, ist die benötigte Zeit erheblich, da in der Regel ein Benutzer aktiv darauf wartet. Ein gewisser Teil an Ressourcen wird bei jeder einzelnen Abfrage verbraucht, da HTTP ziemlich gesprächig ist und obenrein noch auf dem relativ schwerlastigen TCP aufbaut. Dieser Verbrauch lässt sich nicht vermeiden. Je weniger jedoch für eine Abfrage neu berechnet werden muss, desto mehr Ressourcen werden eingespart.
Vorberechnung von Inhalten
Besonders bei lesendem Zugriff auf Inhalte, die nicht zwangsweise hochaktuell sein müssen, bietet es sich an, bereits vorgerenderte Inhalte auszuliefern. Beispielsweise reicht es bei den meisten Statistikausgaben aus, diese höchstens alle n Zeiteinheiten neuzuerstellen. Ein weiteres Beispiel ist die Einbindung externer Inhalte. Während der Benutzer auf die Abfrage wartet, ist es unvorteilhaft, erst einen externen Rechner einer anderen (administrativen, lokalen, …) Domäne nach Inhalten zu befragen. So kann z.B. die Einbindung von Mikrobloginhalten sonst zu erheblichen Wartezeiten führen, wenn dieser externe Dienst einmal nur langsam oder garnicht erreichbar ist.
Arbeit verteilen
Aus performancetechnischer Sicht ist es nicht zweckmäßig, Webserver mit etwas anderem als dem direkten Ausliefern von Inhalten zu beauftragen. Dies merkt man besonders, wenn während hunderten Benutzeranfragen jeweils, x-tausend Datensätze verarbeitet werden müssen. Besser ist es, diese Arbeit an andere Programme oder Rechner zu verteilen, die darauf spezialisiert sind, diese Verarbeitung durchzuführen. Verteilung selbstzubauen ist nicht trivial, da es dabei eine sehr große Menge Fehlerfälle gibt, die gefangen gehören. Man kann aber die Verteilung auch mit einem Framework wie Gearman lösen. Der Vorteil dabei ist, dass man die eigene Anwendungslogik von der Gearman-Verteilungslogik klar trennen kann. Geschenkt erhält man dabei unter anderem Skalierbarkeit und Fehlertoleranz. Man kann also einfach bei Bedarf "Lastesel" hinzufügen oder entfernen, um mehr Rechenkapazität für den verteilten Code zu erhalten. Weiter fällt es kaum ins Gewicht, wenn einmal einer der "Lastesel" stirbt, da ja noch genügend andere seine Arbeit übernehmen können. Der Nachteil ist der erhöhte Verwaltungsaufwand der "Lastesel".
Eine weitere Möglichkeit ist die Verteilung von unterschiedlichen Inhalten auf physikalisch getrennte Rechner. Beispielsweise könnte man einen oder mehrere Bilder-, Javascript-, Videoserver betreiben. Deren Integration kann dann im eigentlichen HTML erfolgen. Das bedeutet, dass z.B. drei Bilderserver per Round Robin-DNS die gleiche Arbeit verrichten und über bilder.DOMAIN.TLD angesprochen werden. Fällt einen von denen aus, ist es nicht schlimm. Fallen alle aus, sieht die Applikation etwas spartanischer aus, aber sie läuft weiter. Der HTML-Webserver kann sich so entspannt dem Ausliefern von reinen HTML-Inhalten widmen.
Lastabhängig arbeiten
Es ist zwar schön, wenn man eine funktionsträchtige, bunte, … Webapplikation gebaut hat, aber zu Spitzenlastzeiten, kann jedes noch so effiziente System einmal gemütlich werden. Ein Ansatz ist es, von Anfang an verschiedene "Featurestufen" in der Applikation zu realisieren. Die Last sämtlicher kritischer Systeme wird permanent überwacht und bei einem Engpass wird automatisch eine Featurestufe heruntergeschaltet, um die Last zu reduzieren und notwendige Ressourcen freizugeben. Mögliche Featurestufen sehen z.B. wie folgt aus.
- HTML ohne weitere externe evtl. nachzuladende Einbindungen und ReadOnly-Zugriff (minimale Features)
- HTML und CSS ohne Bilder und ReadOnly-Zugriff
- HTML, CSS, Bilder
- HTML, CSS, JavaScript, Bilder
- HTML, CSS, JavaScript, Bilder, Videos, … (maximale Features)
Klar ist, dass je tiefer die Featurestufe ist, desto potentiell unschöner fühlt sich alles an. Fakt ist jedoch, dass die Verfügbarkeit über einen verhältnismäßig breiten Lastbereich hinweg gewährleistet ist.
Cachingstufen
Besonders bei zahlreichen ReadOnly-Zugriffen, kann man Cachinghierarchien einführen. Stark belastete Teilsysteme wie Webserver und Datenbanken können so vor der Beantwortung von Lesezugriffen geschützt werden und sich komplexeren Aufgaben wie schreibenden oder zustandsabhängigen Zugriffen widmen. So können Proxies wie Squid vor potentiell langsame Webserver geschalten werden. Das perfekte Beispiel ist für mich der Webserver eines Wikis, welcher sich nur noch kaum um profane Seitenaufrufe kümmern muss, wenn eine solche Cachingstrategie gefahren wird. Der Nachteil bei diesem Ansatz kann die verzögerte Auslieferung von neuen Inhalten sein. Dies tritt immer dann auf, wenn der Proxy die Änderung eines Datensegements nicht mitbekommen hat und so lange "alte" Daten ausliefert, bis das Maximalalter des jeweiligen Datensegments erreicht und ein Neuladen erzwungen wurde.
Abfragen sparen und Inhalte komprimieren
Wenn man eine HTML-Seite z.B. so aufbaut, dass zehn verschiedene CSS-, fünf JavaScript-Dateien, ein Favicon und 25 Bilder vom Client potentiell nachgeladen werden müssen, so wird der Seitenaufbau unweigerlich gemütlich werden. Fakt ist, dass jede Abfrage einen gewissen Overhead mit sich bringt, der in großer Menge erheblich sein kann.
Eine Verringerung erreicht man, indem Inhalte zusammengefasst werden. CSS und JavaScript kann man theoretisch in jeweils eine große Datei packen, welche in jeweils einer Abfrage übertragen werden kann.
Auch Bilder lassen sich z.B. mit CSS-Sprites zusammenfassen. Ein Favicon kann man wunderbar Base64-kodiert ins HTML verfrachten, da die meist nur wenigen Bytes eines solchen Minibildes nach der Komprimierung den unkomprimierten HTML-Header unverhältnismäßig aussehen lassen. Auch diese Technik setze auf allen Seiten dieser Site ein.
Sicherstellen sollte man, dass für sämtliche textbasierten Inhalte die gzip-Komprimierung des Webservers aktiviert ist. Die meisten Clients unterstützen diese Betriebsart und es wäre eine Verschwendung an teurem Trafficvolumen und Bandbreite, dies nicht einzusetzen.
Fazit
In jeder Website und Webapplikation steckt ein gewisses Optimierungspotential und bei Bedarf kann man mit wenigen Handgriffen schon viel erreichen, um den Nutzern und den eigenen Systemen das Leben angenehmer zu machen. Welche Strategien man anwendet, ist sehr stark situationsabhängig und deren wohlüberlegte Wahl ist entscheidend. Ich hoffe, dass ich keine wichtigen Aspekte der Session vergessen habe und bedanke mich nochmal bei den Teilnehmern für die konstruktive Stunde in diesem Themenbereich.
Anmerkung
In diesem Blogbeitrag habe ich Systemressourcen der Wichtigkeit geordnet nach Netzwerk, Prozessor, Hauptspeicher und Massenspeicher betrachtet. Ob dies für jeden anderen Fall angemessen ist, muss individuell entschieden werden.
Kommentare