PHP und Apache-Webserver für optimale Leistung einrichten

Der Schriftsteller Karl May, hier in der Pose von Old Shatterhand, war Schöpfer der Romanfigur Winnetou, aber den Apache-Webserver hat er nicht programmiert. Foto: Alois Schiesser/Wikimedia Commons, Public Domain

Es war schon immer ziemlich einfach, die Skriptsprache PHP als Modul des Apache-Webservers zum Laufen zu bringen. Kurz gesagt, auf einer LAMP-Box (Linux, Apache, MySQL, PHP) mit Debian oder Ubuntu genügt dafür ein apt-install-Einzeiler in der Konsole. Für diese Einfachheit muss der Server-Admin aber einen Preis zahlen. Sie kostet nämlich Ressourcen, vor allem: Arbeitsspeicher: Um PHP als Modul einzubinden, nutzt der Apache das Multi Processing Modul (MPM) Prefork. Es startet für jede Anfrage eines Clients, in erster Linie also des Web-Browsers eines Besuchers der Website, einen eigenen eigenen Prozess („forken“), einige davon vorsorglich, um eine schnellere Bearbeitung neuer Anfragen zu ermöglichen. Jeder Prozess lädt dazu noch das PHP-Modul, selbst wenn dann nur eine Bilddatei angefordert wird, für deren Auslieferung der PHP-Interpreter gar nicht benötigt wird.

Wenn der Server viele Abfragen zur gleichen Zeit beantworten muss, dann laufen auch viele Prozesse; so kann der Arbeitsspeicher schnell knapp werden, bis der Server „swappen“ muss (also die Auslagerungsdatei auf der Festplatte als Zwischenspeicher verwendet) und damit komplett ausgebremst wird. Wenn schließlich der Linux-Kernel aus Speichermangel Prozesse abschießt und beispielsweise die auf demselben Server laufende Datenbank lahmlegt, ist die Website entgültig down.

Prefork und Event, Prozesse und Threads

Zum Glück gibt es eine Alternative: PHP-FPM. FPM steht für FastCGI Process Manager“. Damit läuft PHP nicht mehr als Modul innerhalb des Webservers. Vielmehr leitet der Apache über ein Proxy-Modul alle Anfragen, die eine Abarbeitung durch PHP erfordern, an PHP-FPM weiter. MPM Prefork kann entsorgt werden, denn PHP-FPM läuft unter dem erheblich leistungsfähigeren MPM Event. Es startet nur wenige Kind-Prozesse, kann aber trotzdem viele Verbindungen zur gleichen Zeit abarbeiten, weil in jedem Prozess mehrere Threads laufen, die sich Programmbibliotheken teilen und somit sparsamer mit dem Arbeitsspeicher umgehen.

Each child process creates a fixed number of server threads as specified in the ThreadsPerChild directive, as well as a listener thread which listens for connections and passes them to a worker thread for processing when they arrive.
Apache-Dokumentation zu MPM Event

Das alte PHP-Modul dagegen ist nicht Thread-sicher und kann deshalb nur unter dem MPM Prefork betrieben werden. Umgekehrt ist es nicht möglich, das perfomantere HTTP/2-Protokoll mit MPM Prefork zu betreiben. Prefork kann eben immer nur eine Anfrage pro Verbindung ausführen, während HTTP/2 mehrere Streams zur gleichen Zeit über eine Leitung schickt. Ein Vorteil von Prefork soll nicht verschwiegen werden, die Isolierung der einzelnen Prozesse: Sollte ein Prozess abstürzen, laufen alle anderen Prozesse weiter; wenn dagegen mal ein Thread abstürzt, stürzen auch alle anderen Threads innerhalb desselben Prozesses ab.

PHP via FastCGI einzubinden, ist keine Frickelei mehr

Früher war es eine ziemliche Frickelei, PHP via FastCGI einzubinden. PHP-FPM räumt mit all diesen Problemen auf. Dummerweise wirft der entsprechende Eintrag in Apaches Httpd-Wiki mehr Fragen auf, als dass er zur Konfigurations-Referenz taugte, und die PHP-Dokumentation für die Installation mit Apache ist hoffnungslos veraltet. Deshalb an dieser Stelle eine kleine, aktualisierte Checkliste der Dos and Don’ts:

  • Um Anfragen an PHP-FPM weiterzuleiten, das Apache-Modul mod_proxy_fcgi verwenden, nicht mod_fastcgi.
  • Zur Weiterleitung die Direktive FilesMatch mit SetHandler verwenden – nicht ProxyPassMatch, sonst ist kein URL-Rewriting mehr möglich; ProxyPassMatch wird sofort ausgeführt.
  • Laufen Webserver und PHP-FPM auf derselben Maschine, kann die Kommunikation über einen UNIX-Socket laufen; den TCP-Port spart man sich.

Debian 9 und Ubuntu 16.04.3 liefern PHP-FPM fast schlüsselfertig

Soweit die graue Therorie. Die gute Nachricht für die Praxis ist, dass Debian 9 (erschienen am 17. Juni 2017) und auch Ubuntu 16.04.3 (Update erschienen am 5. August 2017) den Apachen mit PHP-FPM 7 so gut wie schlüsselfertig ausliefern. Und auch dafür ist ein apt-install-Einzeiler ausreichend; anschließend muss allerdings noch die bereits enthaltene Konfigurationsdatei aktiviert werden. Wermutstropfen: Installiert man den LAMP-Server per tasksel, spielt Debian doch wieder die alte Variante mit PHP-Modul und MPM Prefork ein.

Was aber ist mit Debian 8? Auf der 2014 erschienene Version, inzwischen nur noch „oldstable“, aber sicher noch auf vielen Servern am Werkeln, kann man mit etwas Mehrarbeit ebenfalls PHP-FPM scharf schalten. Eine Standard-Konfigurationsdatei fehlt, aber man kann diejenige aus Debian 9 übernehmen. Selbst ein Umstieg im Live-Betrieb ist fast übergangslos möglich (Apache muss einmal neu gestartet werden).

smem errechnet den tatsächlichen Speicherbedarf aller Apache-Prozesse

Was bringt’s? Zum Test habe ich zwei Debian-9-Instanzen in VirtualBox frisch aufgesetzt – eine mit MPM Prefork und PHP-Modul, die andere mit MPM Event und PHP-FPM. In beiden Fällen wurde die bei Debian mitgelieferte Standard-Konfiguration nicht verändert. Zum Test habe ich außerdem eine WordPress-Website installiert und die Homepage 1000-mal über SSL von dem Test-Werkzeug Apachebench aufrufen lassen, einmal mit 10, einmal mit 40 parallelen Requests, um unterschiedliche Lastsituationen zu simulieren. Als Response wurden vom Server jeweils 14 MB zurückgeliefert. Um auch eine statische Ressource auszuliefern, habe ich nach demselben Muster eine 80 MB große Bilddatei 6000-mal abrufen lassen.

Ausschnitt aus dem Testergebnissen von Apachebench, hier für 10 parallele Requests einer PHP-Seite.

Interessant war die Frage, wie der Speicherbedarf möglichst realistisch zu ermitteln wäre. Nach einiger Recherche habe ich mich für das Linux-Tool smem entschieden. Warum nicht für top oder ps? Weil smem tatsächlich errechnet, wieviel Arbeitsspeicher ein Prozess in Beschlag nimmt (Spalte PSS – proportional set size). Die anderen Prozessmonitore ermitteln dagegen viel zu hohe Werte (Spalte RES oder RSS), weil sie den Speicher für Bibliotheken, die sich mehrere Prozesse teilen, nicht anteilig veranschlagen, sondern jedem Prozess voll anrechnen.

Mit top sieht man schnell und in Echtzeit die laufenden Prozesse und kann diese auch nach Speicherbedarf sortieren (top -o +RES), aber smem misst besser. Das Gesamtbild stimmt jedoch: Der MySQL-Prozess benötigt den meisten Speicher. PHP-FPM hat unter zum Zeitpunkt dieses Screenshots vergleichsweise höherer Last (load average) fünf Kindprozesse erzeugt; der Apache mit zwei Kindprozessen liegt weit darunter.

Mit dem Tool smemstat kann man sich zusätzlich die Summen aller Prozesse ausgeben lassen (oder mit dem Schalter -p nur ausgewählte Prozesse), für den Apachen also:

$ smemstat -p apache2

Hier sind die Test-Ergebnisse, wobei für den Betrieb mit PHP-FPM zusätzlich zum Speicherbedarf des Apachen auch jener von PHP-FPM ermittelt werden musste:

Laufzeit Requests/s Speicherbedarf (PSS) Prozesse
Apache PHP-FPM Apache PHP-FPM
Apache mit PHP-Modul
WordPress 10/1000 64,431 s 15,52 195,3 MB n/A 1+16 n/A
WordPress 40/1000 67,805 s 14,75 520,7 MB n/A 1+50 n/A
Bild 10/6000 64,488 s 93,94 33,6 MB n/A 1+20 n/A
Bild 40/6000 65,100 s 92,17 44,9 MB n/A 1+52 n/A
Apache mit PHP-FPM
WordPress 10/1000 64,993 s 15,39 10,7 MB 82,5 MB 1+2 1+5
WordPress 40/1000 65,140 s 15,35 17,1 MB 82,7 MB 1+3 1+5
Bild 10/6000 62.428 s 96.11 11,0 MB 27,2 MB 1+2 1+2
Bild 40/6000 64,075 s 93,64 17,0 MB 27,3 MB 1+3 1+2

Die Spalten Laufzeit und Request pro Sekunde geben an, wie lange ein Test von Apachebench durchgelaufen ist und wie viele Anfragen pro Sekunde der Webserver geschafft hat. Wie man sieht, ist Apaches Event-Modul mit PHP-FPM selbst bei 40 parallelen Requests nicht schneller als die traditionelle Variante. Die Zeit-Unterschiede in der Tabelle dürfen getrost vernachlässigt werden, zumal die Laufzeiten bei mehreren Durchläufen eine Schwankungsbreite von mehreren Sekunden aufwiesen. Spürbar schneller wird eine mit PHP-FPM und MPM Event ausgelieferte Website selbst unter höherer Last also nicht!

Auch beim Abruf des Bildes finden sich keine gravierenden Unterschiede. Der Apache mit PHP-Modul forkt zwar viele Prozesse; die benötigen aber nicht viel Speicher. Mit MPM Event beansprucht der Apache zwar noch weniger Speicher, aber es befinden sich auch noch PHP-FPM-Prozesse im RAM, die für das Bild zwar nicht gebraucht werden, aber dennoch auf Arbeit warten und Speicherplatz belegen.

Wenn’s aber richtig Arbeit gibt, wenn keine statischen Ressourcen, sondern dynamische PHP-Seiten ausgeliefert werden müssen, dann spielt PHP-FPM seine Stärken in Sachen Speicherbedarf aus – je mehr parallele Requests stattfinden, um so höher fällt der Gewinn aus. Die Apachebench-Tests zeigen, dass bei 40 parallelen Abfragen einer WordPress-Seite der Apache mit PHP-Modul gut 520 MB okkupiert, also mehr als die Hälfte des Arbeitsspeichers der virtuellen Maschine. Dagegen benötigen Apache und PHP-FPM zusammen nur 17+82 MB, also etwa ein Zehntel des Gesamtspeichers.

Natürlich wird ein einzelner WordPress-Blog 40 parallele Requests im normalen Leben kaum erreichen. Aber wenn auf einem Webserver mehrere Websites als virtuelle Hosts laufen oder wenn unerwartete Lastspitzen eintreten, dann bietet PHP-FPM viel mehr Reserven.

Mit MaxRequestWorkers am Speicherbedarf schrauben

Grundsätzlich ist es so, dass der von den Linux-Distibutionen ausgelieferte Apache bereits mit praxisgerechten Werten konfiguriert ist, um eine gute Leistung im Betrieb zu liefern. Trotzdem kann, wer viel Arbeitsspeicher zur Verfügung hat, den Apachen noch großzügiger konfigurieren. Umgekehrt: Wer Performance-Probleme oder gar Abstürze beobachtet, kann in einem ersten Schritt die Ressourcen begrenzen. Beste Stellschraube ist der Wert MaxRequestWorkers. Wer noch einen mit PHP-Modul laufenden Webserver betreibt, findet diese Einstellung in der Konfigurationsdatei /etc/apache2/mods-available/mpm_prefork.conf:

# StartServers: number of server processes to start
# MinSpareServers: minimum number of server processes which are kept spare
# MaxSpareServers: maximum number of server processes which are kept spare
# MaxRequestWorkers: maximum number of server processes allowed to start
# MaxConnectionsPerChild: maximum number of requests a server process serves

<IfModule mpm_prefork_module>
 StartServers 5
 MinSpareServers 5
 MaxSpareServers 10
 MaxRequestWorkers 150
 MaxConnectionsPerChild 0
 </IfModule>

Standardwert ist 150, das heißt, der Apache darf maximal 150 Verbindungen gleichzeitig aufbauen. Da das Prefork-Modul für jede Verbindung einen eigenen Prozess benötigt, kann der Apache also bis zu 150 Mal forken. Als nächstes ermittelt man mit smem oder smemstat den Speicherbedarf eines typischen Apache-Prozesses im laufenden Betrieb. Angenommen, das sind 20 MB. Dann würde der Webserver unter maximaler Auslastung 150*20 MB, also 3000 MB, beanspruchen. In diesem Fall sollte schon eine Maschine mit 4 GB Arbeitsspeicher zur Verfügung stehen, zumal der Datenbankserver und andere Dienste ebenfalls Arbeitsspeicher benötigen, der hier noch gar nicht berücksichtigt ist.

Faustregel für die Anpassung der Konfiguration:
([Gesamtspeicher] – [Speicherbedarf anderer Dienste]) / [Speicherbedarf pro Apache-Prozess] = [MaxRequestWorkers]

Die Kosten/Nutzen-Bilanz von KeepAlive

Noch ein Wort zum in der Basis-Konfigurationsdatei /etc/apache2/apache.conf festgelegten KeepAlive. Standardwert für den KeepAliveTimeout sind fünf Sekunden. Manchmal liest man die Empfehlung, den KeepAlive bei Speichermangel ganz abzuschalten („off“). Das ist aber keine brilliante Idee. Der KeepAlive sorgt für eine persistente (aufrecht erhaltene) Verbindung zwischen Server und Client. Was heißt das und was bringt das? Eine Webseite wie diese hier besteht aus Dutzenden von Ressourcen (PHP, Bilder, CSS, Javascript), die der Browser nach Verbindungsaufnahme mit dem Server in einem Rutsch laden kann – eben weil der Server die Verbindung nicht jedes Mal wieder schließt, sondern für die in KeepAliveTimeout definierte Zeit aufrecht erhält. Klickt ein Besucher innerhalb des KeepAliveTimeouts noch auf einen weiteren Link oder werden in diesem Zeitraum weitere Ressourcen per Ajax nachgeladen, dann läuft das ebenfalls noch innerhalb der aktuellen Verbindung.

Persistenz ist also tatsächlich nützlich, um die Geschwindigkeit der Auslieferung einer Website zu erhöhen. Besonders bei sicheren Verbindungen spart man sich jede Menge Protokoll-Overhead bei der Kommunikation mit dem Client. Trotzdem sollte man dem Wunsch wiederstehen, den Timeout einfach mal so in schwindelnde Höhen zu schrauben. Denn 150 in MaxRequestWorkers definierte gleichzeitige Verbindungen sind gar nicht mehr so viele, bedenkt man die Kosten von KeepAlive: dass nämlich jede dieser Verbindungen erst nach fünf Sekunden wieder frei wird, so dass der 151. Besucher länger warten muss, bis sich in seinem Browser etwas tut.

Die gute Nachricht ist, dass die gesamte Keep-Alive-Problematik mit HTTP/2 verschwindet wie Nebel im Sonnenschein, weil HTTP/2 – siehe oben – nicht mehr auf eine Abfrage pro Verbindung limitiert ist.

Wann es Zeit wird, auf PHP-FPM umzurüsten

Stößt man mit den MaxRequestWorkers und einem praxisgerechten KeepAlive an Grenzen, dann, spätestens dann sollte man wirklich auf PHP-FPM umrüsten, um wieder Land zu gewinnen und im Apachen HTTP/2 aktivieren. Ein Blick in die Standard-Konfigurationsdatei von MPM Event unter /etc/apache2/mods-available/mpm_event.conf zeigt, dass der Apache 25 Threads pro Kind-Prozess (ThreadsPerChild) erzeugt und maximal 150 Verbindungen (MaxRequestWorkers) gleichzeitig bedienen darf; maximal erzeugt er also 150 / 25 = 6 Prozesse.

# StartServers: initial number of server processes to start
# MinSpareThreads: minimum number of worker threads which are kept spare
# MaxSpareThreads: maximum number of worker threads which are kept spare
# ThreadsPerChild: constant number of worker threads in each server process
# MaxRequestWorkers: maximum number of worker threads
# MaxConnectionsPerChild: maximum number of requests a server process serves

<IfModule mpm_event_module>
 StartServers 2
 MinSpareThreads 25
 MaxSpareThreads 75
 ThreadLimit 64
 ThreadsPerChild 25
 MaxRequestWorkers 150
 MaxConnectionsPerChild 0
 </IfModule>

Sind das die idealen Werte? Schwer zu sagen. Während man – wie wir zuvor gesehen haben – den Speicherbedarf von Apache Prefork mit PHP-Modul noch relativ einfach hochrechnen kann, müssen beim Betrieb von Apache Event mit PHP-FPM beide Dienste im Auge behalten werden. Wie die Benchmarks mit WordPress zeigen, ist PHP-FPM der viel größere Speicherfresser – und damit die erste Instanz, an der es zu schrauben gilt, sollte die LAMP-Box tatsächlich an Limits stoßen. Ein Allheilmittel ist auch das nicht. Bei schnell wachsenden Websites kommt bald der Punkt, wo ein Server nicht ausreicht. Dann muss man eine Lastverteilung auf mehrere Server organisieren – zum Beispiel über einen Proxy wie Squid.

Trotzdem wird es viele Admins geben, die mit ihrem eigenen Server im Netz noch nie an Performance-Grenzen gestoßen sind – selbst dann nicht, wenn dieser Server noch in althergebrachter Art mit MPM Prefork samt PHP-Modul aufgesetzt ist. Zeitgemäß ist dieses Setup absolut nicht mehr, obwohl Debian und Ubuntu es immer noch standardmäßig installieren. Wenn der Apache sich damit bescheiden darf, nur das zu machen, was ein Web-Server eigentlich machen sollte – nämlich über eine HTTP-Verbindung statische Dateien auszuliefern, und dies dank HTTP/2 parallel -, und wenn die aufwändige Arbeit des dynamischen Seitenbaus samt Datenbank-Abfragen an PHP-FPM ausgelagert wird, dann spart man Ressourcen und eröffnet dem Server Leistungsreserven.

Wer die eigene Webseiten schneller ausliefern möchte, sollte alledings nicht die Apache-Konfiguration bis zur Erschöpfung tunen, sondern muss Bilder verkleinern, CSS- und Javascript-Dateien minifizieren, seinen PHP-Code optimieren und/oder Caching aktivieren.

Literatur:

2 comments on “PHP und Apache-Webserver für optimale Leistung einrichten”

Schreibe einen Kommentar