Webserver-Frühjahrsputz

Die optimale Systemarchitektur finden
Kommentare

Mit dem Erfolg kommen früher oder später die Probleme: Der irgendwann mal aufgesetzte Server ist der ständig steigenden Last nicht mehr gewachsen. Keine Frage, mehr Hardware muss her! Doch die alte Systemarchitektur lässt die Verteilung der Last auf mehrere Server nur sehr bedingt zu. Der ideale Zeitpunkt also, die Architektur zu überdenken und dabei das Ganze auch auf der Softwareseite auf ein neues Fundament zu stellen.

Moderne Webanwendungen sind weit weg von dem, was den LAMP-Stack einst hat groß und erfolgreich werden lassen: Vorbei die Zeit, in der man statistische Seiten mit ein wenig integriertem PHP aufgepeppt hat und somit die volle Kontrolle und vor allem die Hauptarbeit weiterhin beim Webserver lag. Heutige Anwendungen sind größer und komplexer geworden, bedürfen vielschichtigerer Infrastrukturen. Wer heute noch PHP mit HTML vermischt, gilt als rückständig – selbst wenn PHP unabhängig vom Rest der Anwendung auch als Templatesprache eine durchaus gute Figur machen kann. Betrachtet man den strukturellen Aufbau aktueller Webseiten jedoch einmal unabhängig von der eingesetzten Templatesprache und -Engine und aus der Sicht des Browsers, wird man schnell erkennen, dass auf den initialen Seitenabruf eine Vielzahl an weiteren Anfragen an den oder die Webserver geschickt werden müssen. Da wären zunächst die eingebundenen Bilder, CSS-Sprites oder Icons sowie die CSS-Regeln. Letztere werden dabei oft und gerne in verschiedene Dateien – passend für die jeweiligen Browser – aufgeteilt. Und auch JavaScript darf heute in einer modernen Anwendung natürlich nicht fehlen. Weitere Dateien also, die der Client nachladen muss.

Webserver haben heute also mehr denn je zu tun, müssen sie doch das ständige Mehr an Daten über die Leitung schicken. Doch obwohl die zu übertragenden Datenmengen steigen, spielen die in den Server direkt integrierten Skriptsprachen bei der Hauptarbeit eine immer kleinere Rolle. Diese Erkenntnis stellt das Konzept einer in den Webserver eingebundenen Laufzeitumgebung wie PHP ganz deutlich in Frage: Warum muss diese Runtime für jeden Request, egal ob er durch PHP verarbeitet werden wird, instantiiert werden? Vor allem inklusive all der von PHP selbst dynamisch nachgeladenen Erweiterungen?

Wir brauchen ein CDN – oder?

Eine mögliche Konsequenz ist die Einführung eines so genannten Content Delivery Networks – kurz CDN –, um die Application Server von den Zugriffen auf statische Inhalte zu entlasten. Ein derartiges System kann, gerne auch geografisch verteilt, die Auslieferung der Inhalte ohne zusätzlichen und unnötigen Overhead vornehmen. Ideal und leicht umzusetzen, wenn es keinerlei zu prüfende Beschränkungen für den Zugriff gibt, wie dies zum Beispiel für Produktbilder in einem Shop der Fall sein dürfte. Schwieriger wird es erst, wenn die Auslieferung vom Nutzerstatus oder anderweitigen Bedingungen abhängt: Soll hier mehr als nur die Client-IP geprüft oder eine klassische HTTP-Authentifizierung durchgeführt werden, kommt man mit den Board-Mitteln der meisten Webserver nicht weiter. Hier müsste offensichtlich eine im CDN angesiedelte leistungsfähigere Logik die Prüfung vornehmen, will man die Trennung zwischen Applikation und CDN aufrecht erhalten.

Also doch wieder alles mit PHP ausliefern? Weit gefehlt! Denn trennt man den Webserver logisch von der Ausführung von PHP, so braucht ein PHP-Skript nicht zwingend eine eigene Ausgabe an den Client zu schicken, sondern kann die Kontrolle an den Webserver zurückgeben. Und diesem gleichzeitig mitteilen, welche statische Datei übertragen werden soll – oder ob der Zugriff verweigert wurde. Dieser Ansatz wird vor allem dann interessant, wenn die logische Trennung auch technisch vollzogen wurde: Laufen Webserver und PHP in getrennten Prozessen, so ist der Pool an aktiven PHP-Instanzen nach der Entscheidung für oder gegen das Ausliefern wieder für andere Aufgaben einsetzbar. Und der Webserver kann das tun, wofür er primär entwickelt wurde – hochperformant Dateien ausliefern.

Mit PHP-FPM zurück in die Zukunft

Damit das in der Praxis sinnvoll funktionieren kann, braucht es eine alternative Anbindung von PHP als die altbekannte Einbindung als Modul in den (Apache-)Webserver. Jahrelang führte das schon immer sehr leistungsfähige Protokoll FastCGI (Kasten: „FastCGI“) im PHP-Universum ein Schattendasein. Erst mit zunehmender Verbreitung alternativer Webserver wie NGINX und lighttpd erwachte auch das Interesse am FastCGI-Protokoll erneut. Um PHP über FastCGI ansprechen zu können, wurden anfangs zum Teil abenteuerliche Konstrukte in Form diverser Shell-Skripte entwickelt, die das Starten und Stoppen des PHP-Prozesses übernahmen. Alternativ konnte und kann diese Verwaltung auch über „spawn-fcgi“ – ein Unterprojekt des lighttpd-Webservers – übernommen werden. Mit der Übernahme des vorher abseits von PHP gepflegten Patches zum FastCGI-Prozessmanagement (PHP-FPM) in den PHP-Kern steht nun eine native und von den Kernentwicklern unterstütze Variante bereit, die immer beliebter wird.

Vor allem in Kombination mit leichtgewichtigeren Webservern wie NGINX spielt dieses neue Set-up seine Stärken aus. Diverse Benchmarks belegen beispielsweise eindrucksvoll, dass die Kombination aus PHP und NGINX eine deutlich bessere Auslastung der Hardwareressourcen ermöglicht als es das klassische Duo PHP und Apache-Webserver vermag. Da sowohl NGINX als auch PHP mit FPM zudem inzwischen in allen größeren Linux-Distributionen enthalten sind, ist auch die grundlegende Installation denkbar einfach. Unter CentOS/Fedora reicht das dort gewohnte yum, um das Package Management davon zu überzeugen, dass besagte Software auf dem System eingerichtet werden soll: yum install nginx php-fpm.

Und auch die eigentliche Konfiguration ist schnell gemacht und geht in einer ersten Version leicht von der Hand: Wie in Listing 1 gezeigt, besteht die Einbindung von PHP in einen NGINX vHost aus wenigen Zeilen. Zunächst wird ein weiterer Location-Abschnitt in die passende Serversektion der NGINX-Konfiguration eingefügt. Aufgrund des regulären Ausdrucks gilt dieser für alle Dateinamen bzw. URLs, die auf php enden. Derartige Anfragen werden als dann per FastCGI an den festgelegten Service weitergereicht – hier der lokal auf Port 9000 lauschende PHP-FPM. Neben den von NGINX bereits vordefinierten und per include nachgeladenen Parametern setzen wir hier nur noch den Pfad zur aufzurufenden PHP-Datei, bevor der Request auch wirklich beim PHP-FPM ankommt.

Listing 1

server {
  listen      80;
  server_name localhost; 
  location / {
    root    /var/www/htdocs;
    index   index.php;
  }

  # Sicherheitshinweis:
  # In der php.ini den Schalter cgi.fix_pathinfo auf "off" stellen
  location ~ .php$ {
    fastcgi_pass    127.0.0.1:9000;
    # Alternativ lokal per Unix-Socket
    # fastcgi_pass   unix:/var/run/php-fpm/php-fpm.sock;
    include         fastcgi_params;
    fastcgi_index   index.php;
    fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param   SCRIPT_NAME $fastcgi_script_name;
  }
}

So einfach diese Konfiguration bereits ist, sie lässt sich weiter optimieren. Vorausgesetzt, man verwendet eine halbwegs moderne Softwarearchitektur mit genau einer zentralen Bootstrap-Datei. Da in einem solchen Szenario nur genau eine PHP-Datei direkt von außen „erreichbar“ ist, benötigen wir das generische Mapping aus Listing 1 nicht mehr. Ebenfalls stellt sich die Frage, ob diese Bootstrap-Datei überhaupt noch im Document-Root, dem von außen erreichbaren Verzeichnis des Webservers, liegen muss. Und die klare Antwort ist: nein. Wozu auch, schließlich hat der Webserver in unserer neuen Welt ja mit der Ausführung des Skripts nichts mehr zu tun. Listing 2 zeigt eine derartig angepasste Konfiguration: Mittels der Direktive try_files wird der Server angewiesen zu prüfen, ob es sich bei dem angefragten URI um eine der im Document-Root hinterlegten Dateien handelt, was in der Regel auf statische Inhalte wie Bilder, CSS und JavaScript zutrifft. Sollte sich keine passende Datei ermitteln lassen, wird der Request als vermeintlich für PHP gedacht interpretiert und an das mittels Location definierte alternative Ziel weitergereicht. Der Location-Eintrag besteht im Wesentlichen aus dem jetzt festen und nicht mehr dynamischen Zuweisen des auszuführenden Skripts – hier /var/www/bootstrap.php – sowie dem Überschreiben des originalen Query-Strings. Letzteres ist notwendig, damit der eigentlich angefragte URI zusätzlich zu den gegebenenfalls vorhandenen GET-Parametern übergeben werden kann.

Listing 2

server {
  server_name localhost;
  root /var/www/htdocs;

  location / {
    try_files $uri @php
  }

  location @php {
    fastcgi_pass    127.0.0.1:9000;
    include         fastcgi_params;

    fastcgi_param  SCRIPT_FILENAME  /var/www/bootstrap.php;
    fastcgi_param  SCRIPT_NAME      /bootstrap.php;
    fastcgi_param  QUERY_STRING     uri=$uri&$args;
  }
}

FastCGI

Das Common Gateway Interface – kurz CGI – ist die „Plug-in“-Schnittstelle zu externen Programmen der Webserver und war in den Anfängen des Webs die bevorzugte Methode, externe Prozesse „on Demand“ auszuführen und um dynamische Inhalte zu generieren. Da jedoch für jeden Seitenaufruf ein neuer, externer Prozess gestartet werden musste, wurde dieser Ansatz nicht zuletzt aus Gründen der Performanz in der Breite durch andere Konzepte ersetzt. Im langjährigen Marktführer der Apache Group werden beispielsweise – obwohl auch hier natürlich weiterhin CGI funktioniert – Erweiterungen bevorzugt, als Module in den Server direkt integriert. So auch PHP als mod_php. Da beim Start des jeweiligen Serverprozesses jedoch nicht klar ist, welche Module zur Abarbeitung des Requests benötigt werden, wird auch dieses Konzept inzwischen zumeist eher kritisch gesehen. Zumindest wenn es um das Initiieren teurer Module wie PHP, Python oder Perl geht. Die Antwort auf die obigen Probleme liegt im bereits 1996 entworfenen FastCGI: Anstatt für jede Anfrage einen neuen Prozess explizit zu starten, werden diese im Vorfeld bereits über einen externen Managementdienst instanziiert. Ein eingehender Web-Request kann so nach Bedarf an einen wartenden Prozess delegiert werden, und der Webserver spart sich sowohl unnötige Arbeit als auch Ressourcen für etwaig gar nicht verwendete Module.

PHP-FPM

Der FastCGI-Prozessmanager geht auf einen bereits für PHP 4 verfügbaren Patch zurück, der die Möglichkeiten und die Kontrolle über PHP in FastCGI-Umgebungen verbessert, indem er PHP als eigenständigen Service etabliert und kontrolliert. Viele Dinge, die PHP Out of the Box nicht kannte, wurden möglich: So zum Beispiel das Starten der Worker-Prozesse mit unterschiedlichen UIDs/GIDs, den Einsatz in Chroot-Umgebungen oder mit individuellen php.ini-Optionen – ohne dabei den verpönten Safe Mode zu benötigen. Seit der Übernahme des Patches in den PHP-Kern mit der Version 5.3.3 erfreut sich der PHP-FPM steigender Beliebtheit und wird als die flexibelste und zugleich performanteste Art propagiert, PHP einzusetzen. Angesprochen wird PHP in einem solchen Set-up aus dem Webserver heraus, entweder via lokalem Socket oder über eine IP-Verbindung.

Eine Frage des Routings

Sind Webserver und PHP erst einmal voneinander entkoppelt, werden viele vorher kompliziert wirkende oder gar nicht erst umsetzbare Konfigurationen plötzlich ganz einfach: In Abhängigkeit von URL, Client-IP oder sonstiger Bedingungen kann man beispielsweise die Anfrage gezielt auf passend konfigurierte Instanzen verteilen. Auch dem parallelen Betrieb verschiedener Versionen von PHP steht bei einem derartigen Set-up nichts im Wege, denn die einzelnen Prozesse und Versionen wissen natürlich nichts voneinander. Und in einer NGINX-Konfiguration dürfen selbstverständlich beliebig viele Location-Einträge vorkommen, die wiederum jeweils auf individuelle FastCGI-Anbindungen verweisen können.

Doch es geht noch besser: In der Vergangenheit war PHP unabhängig der Art der An- bzw. Einbindung fast immer eng an den Webserver gekoppelt und wurde somit als quasi technische Abhängigkeit auch auf der gleichen Maschine ausgeführt. Dabei war und ist dies gar nicht zwingend nötig, denn neben dem Unix-typischen Socket kann FastCGI, wie schon in den Listings 1 und 2 gezeigt, über IP und damit auch über Netzwerk angesprochen werden.

Unterstützt von den Load-Balancing-Fähigkeiten moderner Webserver wie NGINX und lighttpd wird damit das Skalieren der Anwendung auf mehrere Systeme zum Kinderspiel. Über die Definition eines Pools lässt sich so zum Beispiel eine ganze PHP-Serverfarm hinter einem oder bei Bedarf auch mehreren Webservern verstecken. Beide Seiten, Web- wie PHP-Server, können hier unabhängig voneinander vergrößert oder verkleinert und in ihrer Menge der Auslastung angepasst werden. Wie eine entsprechend erweiterte Konfiguration für NGINX aussehen kann, zeigt Listing 3: Über die Anweisung upstream wird im Beispiel ein Pool aus drei FastCGI-Servern definiert, wobei der erste Server aufgrund der hohen Gewichtung mit größerer Wahrscheinlichkeit ausgewählt wird als der zweite. Der dritte Servereintrag, markiert mit der Anweisung backup, kommt nur zum Einsatz, wenn einer der vorherigen Server nicht erreichbar sein sollte oder sonstige Probleme bereitet hatte. Die Problemerkennung durch NGINX lässt sich natürlich ebenfalls beeinflussen, wie die Anweisungen max_fails und fail_timeout beim zweiten Server zeigen. Es handelt sich hierbei um die Anzahl an Erreichbarkeitsproblemen, die im angegebenen Zeitraum auftreten dürfen, und um das Zeitfenster, für den dieser Server dann von der Request-Verarbeitung ausgeschlossen wird.

Listing 3

upstream PHP_POOL {
  server 192.168.1.101:9000 weight=10;
  server 192.168.1.102:9000 weight=5 max_fails=3 fail_timeout=30s;
  server 192.168.1.103:9000 backup;
}

server {
  server_name localhost;
  root /var/www/htdocs;

  location / {
    try_files $uri @php
  }

  location @php {
    fastcgi_pass    PHP_POOL;
    include         fastcgi_params;

    fastcgi_param  SCRIPT_FILENAME  /var/www/bootstrap.php;
    fastcgi_param  SCRIPT_NAME      /bootstrap.php;
    fastcgi_param  QUERY_STRING     uri=$uri&$args;
  }
}

Aufgabenteilung

Doch kommen wir zurück zu der Idee, dass der Webserver eine angefragte Datei selbst ausliefern soll und PHP wie eingangs beschrieben lediglich die Entscheidung über den Zugriff fällt. In einer perfekten Welt gäbe es für diesen Mechanismus eine standardisierte, einheitliche technische Lösung. Leider ist dem – wenig überraschend – nicht so, und unterschiedliche Hersteller haben voneinander abweichende Lösungen entwickelt. Glücklicherweise sind sich die Serverhersteller aber zumindest insoweit einig, dass es sich um einen Custom-Header im (HTTP-)Response des FastCGI-Prozesses handeln soll. In diesem vom Server alsdann ausgewerteten Header wird auf eine lokale Datei verwiesen, die ausgeliefert wird. Trotz der unterschiedlichen Implementierungen unterscheidet sich hier zum Glück aus Sicht von PHP nur der Name des zu sendenden Headers, der Rest ist eine serverseitige Einstellung. Tabelle 1 zeigt die je nach Webserver verwendeten Bezeichner und Konfigurationsabhängigkeiten. Auffallend und ein wenig erstaunlich ist, dass sowohl der Marktführer Apache als auch Microsofts IIS erst durch Module von Drittherstellern um diese praktische Funktionalität erweitert werden müssen.

Webserver

Header

Installation/Konfiguration

Apache

X-Sendfile

Über externes Modul „mod_sendfile“ für Apache 2.x, Schalter „XsendFile“ muss auf „on“ gestellt werden

Nginx

X-Accel-Redirect

Pfad muss innerhalb einer als „Internal“ markierten Location liegen

lighttpd 1.4

X-LIGHTTPD-send-file

Die Option „allow-x-send-file“ muss aktiviert sein

lighttpd 1.5

X-Sendfile

IIS

X-Accel-Redirect

X-Sendfile

Über externes Plug-in für IIS 7.x, XSendDir oder XAccelRoot und XAccelLocation

Tabelle 1: HTTP-Header gängiger Webserver

Und die Session-Daten?

Die Verteilung der durch PHP zu verarbeitenden Anfragen auf mehrere Server mag aus Performanzsicht ideal sein, sie wirft aber auch ein neues Problem auf: Der standardmäßig aktive Session-Handler von PHP serialisiert die Session-Daten auf die Festplatte des Web- bzw. PHP-Servers. Mal unabhängig von der Frage, ob dieses Vorgehen zeitgemäß ist, haben wir das Problem, dass aufeinanderfolgende Anfragen nicht unbedingt wieder beim selben Server landen werden. Eine erste Lösungsidee wäre, im Webserver eine Art Mapping zu etablieren, damit eine einmal gestartete Session immer wieder zum selben PHP-Server geleitet wird. Dieses Konzept, das unter der Bezeichnung „Session stickiness“ bekannt ist, hat jedoch gleich mehrere entscheidende Nachteile: Das System kann die Lastverteilung nicht mehr optimal an die Auslastung anpassen, beim Ausfall eines Servers sind alle auf dem System gehosteten Sessions verloren und auch der Verwaltungsaufwand auf dem Webserver steigt deutlich. Wer dennoch diesen Ansatz einmal ausprobieren möchte, kann dieses Verhalten bei NGINX durch die Anweisung iphash innerhalb der Pool-Definition aktivieren.

Eleganter wäre es jedoch, wenn alle im Einsatz befindlichen PHP-Server auf eine gemeinsame Datenquelle für die Session-Verwaltung zugreifen könnten. Damit wäre jeglicher Verwaltungsaufwand im Webserver eliminiert und auch der Wegfall eines PHP-Servers würde faktisch keine Auswirkung auf die grundsätzliche Verfügbarkeit der Anwendung und der Sitzungen haben. Um die gleichen Session-Daten von mehreren Systemen aus lesen und schreiben zu können, bietet sich auf den ersten Blick ein gemeinsam genutztes Verzeichnis an, welches zum Beispiel per NFS eingebunden wird. Was so gut und einfach klingt, hat jedoch dramatische Folgen: Der Abstimmungsaufwand und das NFS-File-Locking geht massiv auf die Performanz des Gesamtsystems und wird auch von den PHP-Kernentwicklern als problematisch eingestuft.

Sieht man sich das Konzept hinter der Speicherung von Session-Daten einmal genauer an, fällt auf, dass es sich um einen klassischen Key-Value-Store handelt. Unter einer Session-ID, dem Key, wird eine Datenstruktur – im Falle von PHP ein assoziatives Array – abgelegt. Warum also nicht einen der hochperformanten Key-Value-Stores als Backend zur Datenspeicherung der Session verwenden? Sowohl Memcache als auch Redis, die bekanntesten Vertreter derartiger Datenbanken, bieten dies über ihre PHP-Erweiterungen bereits an. Die Umstellung vom dateibasierten Speichern ist also lediglich eine Konfigurationsanpassung und erfordert natürlich die Installation eines Memcache- oder Redis-Servers. Sowohl beide Server als auch die jeweiligen Erweiterungen für PHP sind Teil der üblichen Linux-Distributionen und lassen sich in CentOS und Fedora einfach per yum installieren: Für MemCached wären dies yum install php-pecl-memcached bzw. yum install memcached, bei Redis entsprechend yum install php-redis und yum install redis. Je nach gewähltem Backend muss zu guter Letzt in der php.ini lediglich der passende Handler aktiviert werden (Listing 4a/4b).

Listing 4

## a: Auszug der php.ini für Redis als session store
[Session]
session.save_handler = redis
session.save_path = "tcp://192.168.1.200:6379"
## b: Auszug der php.ini für MemCache als session store
[Session]
session.save_handler = memcached
session.save_path = "192.168.1.201:11211"

Wenn nicht jetzt, wann dann?

Wer skalieren muss oder will oder wer einfach nur aus der vorhandenen Hardware besseren Nutzen ziehen will, der ist mit NGINX als Webserver und PHP-FPM als Ausführungschicht gut beraten. Selbst wenn nicht alle Möglichkeiten zur Last- und Aufgabenverteilung von Anfang an benötigt werden, so macht es eine frühzeitige Umstellung später deutlich einfacher, das Set-up auf mehrere verteilte Systeme zu erweitern. Kann auf die ohnehin als problematisch geltenden .htaccess-Dateien des Apache-Webservers verzichtet werden, ist die Einrichtung bzw. Umstellung auf die hier beschriebene Systemarchitektur problemlos möglich und mit erstaunlich wenigen Handgriffen getan. Und auch wer spezielle Funktionen des Apache-Servers benötigt, für die es bei den Konkurrenten keine Entsprechung gibt, kann profitieren. Je nach Anforderung kann ein vorgeschalteter NGINX – ob mit oder ohne Anbindung PHP-FPM – hilfreiche Dienste leisten, indem er Anfragen als Proxy an den dahinter betriebenen Apache weiterleitet. Und auch der Apache-Server kann mittels FastCGI mit einem laufenden PHP-FPM in Kontakt treten – allerdings sind die Konfigurationsmöglichkeiten hier deutlich sparsamer: Das empfohlene Modul kann beispielsweise nur jeweils einen FastCGI-Server pro Konfiguration ansprechen und bietet auch sonst wenig Möglichkeiten, auf etwaige Fehler zu reagieren.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -