Die Technik hinter Apfelwahn
in Linux by Thomas, 23. August 2010 - 4 Comments »
Wie manch einer vielleicht mitbekommen hat, läuft Apfelwahn seit ein paar Wochen auf einem neuen Server bei der Firma Hetzner. Da wir nun das Management des Servers selbst machen, kümmern wir uns nicht nur um den Code der Webseite, sondern auch um die reibungslose Funktion des Webservers an sich. Dabei gab es anfänglich ein paar Schwierigkeiten, die aber jetzt behoben sein sollten. Etwas untypisch für die üblichen Apfelwahn-Themen möchte ich in diesem Artikel auf die Webserver-Technik, die Apfelwahn nun befeuert, eingehen. Leuten, denen sich beim Anblick von kryptischen Konfigurationsdirektiven oder beim Starten des Terminals die Zehennägel aufrollen, lege ich hiermit nahe diesen Artikel zu überspringen.
Installiert man unter Linux einen Apache-Webserver mit PHP, erhält man in der Regel ein Setup, das in gewisser Weise technisch aus dem letzten Jahrtausend stammt. PHP wird als Modul in den Webserver eingebunden und dieser läuft in der “prefork”-Variante, was bedeutet, dass für jede Benutzeranfrage ein dedizierter Apache-Prozess zur Verfügung stehen muss, der in genau diesem Moment nur diese eine Anfrage bearbeiten kann. Moderner wäre die “worker”-Version des Apachen, wo jeder Apache-Prozess in mehreren Threads viele Anfragen gleichzeitig bearbeiten kann. Diese Variante läuft jedoch nicht stabil mit PHP, so dass sie automatisch durch “prefork” ersetzt wird, sobald man das PHP-Modul installiert. Dies ist noch nicht zwingend ein Problem, vor allem nicht, so lange die Webseite nur wenige Besucher hat. Allerdings war es für Apfelwahn ein Problem. WordPress, vor allem in der Multisite-Variante, wie sie hier eingesetzt wird, ist mittlerweile wahrlich kein Leichtgewicht mehr. Dazu kommen diverse Plugins, teilweise selbst programmiert (und daher evtl. nicht unbedingt so ressourcensparend wie möglich
), die allein das Aufrufen der Startseite zu einem Kraftakt für den Webserver machen. Dies führt dazu, dass jeder einzelne Apache-Prozess zwischen 80MB und 120MB an RAM belegt – normal ohne PHP wären ca. 10MB. Jeder kann sich nun wohl selbst ausrechnen, wie viele Besucher in etwa gleichzeitig klicken müssen, damit der RAM eines handelsüblichen Servers auf diese Weise vollständig aufgebraucht ist. Tritt dieser Fall ein, bleibt in den meisten Fällen nur ein Hardreset des ganzen Servers übrig. Der Webserver wird nie mehr mit seinen Anfragen fertig, wenn er auf Daten warten muss, die erst mühselig aus der Swap-Partition der Festplatte hergeschaufelt werden müssen. Dies führt dazu, dass die Benutzer verstärkt anfangen Reload zu drücken, was die Situation natürlich noch weiter verschlimmert. Der Server ist nun so unter Last, dass er auf keine Kommandos mehr reagiert – sofern man sich überhaupt noch einloggen kann.
Diese Situation trat am ersten Tag auf dem neuen Server gleich mehrfach auf. Die erste Abhilfe bestand darin, die Lebenszeit eines Apache-Prozesses auf nur 10-20 Anfragen zu beschränken. Bevor er also zu so einem oben beschriebenen Speichermonster werden kann, wird er bereits wieder beendet und durch einen neuen ersetzt. Dies hat natürlich den Nachteil, dass ein erheblicher Overhead durch das ständige spawnen von neuen Apaches entsteht, was im Extremfall die Antwortzeit auch erheblich vergrößert. Zusätzlich haben wir später noch das WordPress-Plugin W3 Total Cache installiert, welches einmal generierte Seiten zwischenspeichert und beim zweiten Aufruf eines anderen Benutzers als bereits vorliegendes statisches HTML ausliefert, ohne PHP interpretieren zu müssen. Diese beiden Maßnahmen waren erfolgreich, was das totale Einfrieren des Servers betrifft. Hin und wieder kam es aber dennoch zu Lastspitzen, die die Antwortzeiten des Webservers erheblich verlängert haben und die auch einen automatischen Apache-Neustart durch den Überwachungsdaemon “monit” zur Folge hatten, der in solchen Fällen die Notbremse ziehen sollte. Dies war natürlich nach wie vor unbefriedigend.
Die mittlerweile eingesetzte Lösung besteht darin, die PHP-Prozesse vom Apache-Webserver zu trennen. Dies geschieht mit Hilfe des Moduls fcgi, welches die PHP-Ausführung in separate Prozesse auslagert. Mit fcgi könen im Gegensatz zum normalen cgi auch bereits Prozesse “auf Vorrat” gestartet werden. Jeder Prozess kann in mehreren Threads Anfragen parallel bearbeiten, genauso wie der modernere “worker”-Apache, der mit dieser Variante nun auch eingesetzt werden kann. Somit sind nur wenige, kleine Apache-Prozesse notwendig, die statische Inhalte wie CSS, Bilder etc. ausliefern. Dazu kommen einige php-fcgi-Prozesse, die in ihrer Zahl begrenzbar sind. Idealerweise sollte die Anzahl so gewählt werden, dass der verfügbare RAM gerade eben ausgenutzt wird. Dennoch werden alle Besucher bedient, sollte die Besucherzahl höher als die Anzahl PHP-Prozesse sein, da mehrere Anfragen parallel bearbeitet werden können. Es wird einfach langsamer, aber der Server läuft weiter.
Dieses Setup erfordert etwas Konfigurationsarbeit, und läuft nicht einfach durch das reine Installieren à la “apt-get install apache2 php5″. Wer bis zu diesem Punkt gelesen hat, der wird jetzt auch noch die Details des Setups (bezogen auf Debian/Ubuntu) ertragen können:
Zunächst müssen die Pakete libapache2-mod-fcgid und php5-cgi installiert werden, wobei man wie oben beschrieben nicht erschrecken darf, dass Apache-Pakete und das PHP-Modul deinstalliert werden. Dann ist ein kleines Wrapper-Skript /usr/local/bin/php-cgi notwendig, welches später das php-cgi-Executable aufruft und in diesem Fall noch mit einer Umgebungsvariable versorgt:
#!/bin/sh # Set desired PHP_FCGI_* environment variables. # Example: # PHP FastCGI processes exit after 500 requests by default. PHP_FCGI_MAX_REQUESTS=5000 export PHP_FCGI_MAX_REQUESTS # DO NOT SET PHP_FCGI_CHILDREN! # Replace with the path to your FastCGI-enabled PHP executable exec /usr/bin/php-cgi
Des weiteren ist eine Standardkonfiguration für den Apache nötig (/etc/apache2/conf.d/php-fcgi.conf), die dafür sorgt, dass alle php-Aufrufe aller Apache-VHosts an den gerade eben definierten fcgi-Wrapper weitergeleitet werden. Hier werden auch Variablen wie die maximale Laufzeit eines PHP-Prozesses (in diesem Fall 5000 Requests) und die maximale Anzahl PHP-Prozesse (hier aktuell 60) festgelegt.
# Path to php.ini . defaults to /etc/phpX/cgi DefaultInitEnv PHPRC=/etc/php5/cgi # Number of PHP childs that will be launched. Leave undefined to let PHP decide. #DefaultInitEnv PHP_FCGI_CHILDREN 10 # Maximum requests before a process is stopped and a new one is launched DefaultInitEnv PHP_FCGI_MAX_REQUESTS 5000 FcgidMaxRequestsPerProcess 5000 FcgidMaxProcesses 60 <Location /> AddHandler fcgid-script .php Options +ExecCGI FcgidWrapper /usr/local/bin/php5-fcgi .php # Define the MIME-Type for ".php" files AddType application/x-httpd-php .php </Location>
Dies war es auch schon, nach einem Neustart des Apache läuft dieser mit der fcgi-Konfiguration.
Diese Änderung hatte zur Folge, dass Apfelwahn nun vom Seitenaufbau her gefühlt wesentlich flüssiger daherkommt und dass seither auch keine Totalausfälle mehr zu beklagen waren. Zusätzlich sorgt das Paket php5-xcache noch für eine Beschleunigung der PHP-Ausführung. Interessanterweise ist die Performance momentan sogar ohne Einsatz des W3 Total Cache Plug-ins besser als zuvor. Dieses Plug-in sorgt im Zusammenspiel mit der seit gestern aktiven neuen Version von Apfelwahn momentan noch für ein paar unerklärliche Probleme und ist daher bis auf weiteres deaktiviert.

Neueste Kommentare