Donnerstag, 22 Februar, 2018

PHP hat in den letzten Jahren eine deutliche Wandlung vollzogen, um den Anforderungen an eine moderne Infrastrukturen mit gehen zu können. Früher lief PHP vornehmlich als Apache Modul und war damit an die Leistungsfähigkeit des Apaches gebunden. PHP ist nun, seit einigen Jahren, durch FPM stark skalierbar und lässt sich völlig unabhängig von einem bestimmten Webserver einsetzen.

Ich stelle euch in diesem Artikel den Aufbau des FPM 7.0 vor. Ich habe für den Umstieg vom Apache-PHP Modul auf php-fpm viele Artikel gelesen, keiner der von mir gefundenen Artikel war so rund, das ich schnell und eindeutig verstanden hatte, wie der Aufbau und die Funktion war. Daher war es mir wichtig diesen Artikel zu schreiben, damit für euch der Ein/Umstieg schnell, einfach und nachvollziehbar ist.


Installation

Wir benötigen zunächst die Systempakete.

Unter Debian/Ubuntu:

apt-get install php7.0-fpm

Unter Centos/RedHat:

yum install php-fpm

Unter Arch:

pacman -S php-fpm

Im Prinzip war es das schon :) Alles was nun folgt, ist reine Konfiguration und die Anbindung an einen Webserver.


Aufbau

Nach der Installation läuft der Dienst php-fpm lokal unter der IP Adresse 127.0.0.1 auf dem Port 9000 und wartet auf Aufträge. Jetzt könnte man schnell noch einen Eintrag in den Webserver einbauen und den FPM die PHP-Script ausführen lassen, so erklären es die meisten Seiten. Stop, bitte genau so nicht! Wir wollen den Funktionsumfang von FPM nutzen und es nicht wie ein altes Apache-PHP Modul einsetzen.

Nach der Installation haben wir unter /etc/php drei Konfigurationsverzeichnisse: cli, fpm und mods-available. Unter cli liegen die Konfigurationen für die direkte Ausführung auf der Konsole, fpm ist für unser php-fpm und unter mods-available liegen Konfigurationsdateien zu unseren PHP Modulen.

Die beiden php.ini Dateien unter cli wie auch unter fpm, solltet ihr euren grundsätzlichen Anforderungen entsprechend einrichten. Einschränkungen in der fpm/php.ini können später für jeden Pool überschrieben werden, daher macht eure global geltende fpm/php.ini sicher!

Bitte denkt daran, dass ihr benötigte Module wie MySQLi, GD usw. als PHP-Modul installiert und nicht als Webserver-Modul. Der Webserver weiß nichts von seinem PHP-Glück und steht komplett außen vor. Installiert daher auch kein Webserver PHP-Module oder ähnliches.

So, jetzt geht es ins Eingemachte. Unterhalb von fpm liegt ein pool.d Verzeichnis. Hier werden PHP-Server (Pools) angelegt und konfiguriert. Jeder angelegte Pool erhält einen eigenen Port z.B. 9000. Über den jeweiligen Port kann nun ein Webserver alle PHP-Scripte an den PHP-Server übergeben und diese werden entsprechend der Pool-Konfiguration ausgeführt.

Jeder Pool hat eine eigene Konfigurationsdatei unterhalb von fpm/pool.d/ und enthält im groben: den Namen des Pools, die IP-Adresse auf die gehört werden soll, der zu verwendende Port und letztlich die wichtigste Eigenschaft, die Konfiguration der PHP-Prozesse.


Beispiel

Ein Beispiel, um das Konstrukt etwas fassbarer zu machen: Wir wollen ein Forum und ein PhpMyAdmin betreiben. Wir wissen, dass wir unter PhpMyAdmin öfters große Dateien hochladen und die erlaubte Laufzeit für Scripte dazu erhöhen müssen. Uns ist auch gleichzeitig Bewusst, das wir in die Gefahr laufen könnten, das wir mit bestimmten Jobs evtl. das Forum blockieren könnten oder sogar Parameter soweit erhöhen müssen, dass Forenbenutzer in der Lage wären den Server unnötig stark zu belasten. Wir möchten also, dass das Forum und PhpMyAdmin getrennt und unter anderen Voraussetzungen laufen.

Dazu legen wir zwei Pool-Konfigurationen unter fpm/pool.d/ an. Wir nehmen dazu die eindeutigen/griffigen Namen forum.conf und pma.conf

forum.conf
[forum]
; # User
user = forum
group = forum

; # Listen
listen = 127.0.0.1:9000
listen.allowed_clients = 127.0.0.1

; # Process
pm = dynamic
pm.max_children = 15
pm.start_servers = 3
pm.min_spare_servers = 3
pm.max_spare_servers = 5
pma.conf
[pma]
; # User
user = pma
group = pma

; # Listen
listen = 127.0.0.1:9001
listen.allowed_clients = 127.0.0.1

; # Process
pm = dynamic
pm.max_children = 15
pm.start_servers = 3
pm.min_spare_servers = 3
pm.max_spare_servers = 5

Wie ihr sicherlich bemerkt habt, sind die Unterschiede zunächst einmal sehr gering. Der Name des Pools ist anders und unter "listen" sind die Ports unterschiedlich. Ein gewichtiger Unterschied ist das jeder Pool unter einem anderem User und Gruppe läuft. Diese User müssen im System angelegt sein und aus Sicherheitsgründen sollte die Shell bei den Usern auf /bin/false gesetzt werden.

useradd <username> -m -s /bin/false

Wenn ihr nun den PHP-Server neu starten, werden ihr sehen, das dort nun auf Port 9000 und 9001 Services lauschen.

root@host:~# lsof -Pni | grep php
php-fpm  1204     root    8u  IPv4 2059193      0t0  TCP 127.0.0.1:9001 (LISTEN)
php-fpm  1204     root    9u  IPv4 2059194      0t0  TCP 127.0.0.1:9000 (LISTEN)
php-fpm  1207      pma    0u  IPv4 2059193      0t0  TCP 127.0.0.1:9001 (LISTEN)
php-fpm  1208      pma    0u  IPv4 2059193      0t0  TCP 127.0.0.1:9001 (LISTEN)
php-fpm  1209      pma    0u  IPv4 2059193      0t0  TCP 127.0.0.1:9001 (LISTEN)
php-fpm  1212    forum    0u  IPv4 2059194      0t0  TCP 127.0.0.1:9000 (LISTEN)
php-fpm  1213    forum    0u  IPv4 2059194      0t0  TCP 127.0.0.1:9000 (LISTEN)
php-fpm  1214    forum    0u  IPv4 2059194      0t0  TCP 127.0.0.1:9000 (LISTEN)

Schaut euch jetzt mit ps die Prozessliste an, dann werdet ihr ebenfalls sehen, dass die PHP-Server in dem jeweiligen Benutzerkontext gestartet wurden und den Namen des Pools an der Prozessbezeichnung an gehangen wurde.

root@host:~# ps aux | grep php
root         7068  0.0  0.0 ?        Ss  0:14 php-fpm: master process (/etc/php/7.0/fpm/php-fpm.conf)
forum        7071  0.0  0.0 ?        S   0:01 php-fpm: pool forum
forum        7072  0.0  0.0 ?        S   0:01 php-fpm: pool forum
forum        7073  0.0  0.0 ?        S   0:01 php-fpm: pool forum
pma          7076  0.0  0.0 ?        S   0:01 php-fpm: pool pma
pma          7077  0.0  0.0 ?        S   0:01 php-fpm: pool pma
pma          7078  0.0  0.0 ?        S   0:01 php-fpm: pool pma

Optionen

Eine Pool-Konfiguration beinhaltet eine recht übersichtliche Anzahl von Optionen, von denen ich hier einige kurz vorstellen möchte.


Benutzerkontext

Hier könnt ihr definieren, welcher Systemuser und welche Gruppe für die Ausführung des Pools genutzt werden soll

user = <user>
group = <gruppe>

Kommunikation

Ein Pool kann entweder via Socket kommunizieren, über die lokale IP 127.0.0.1 oder über eine "öffentliche" IP des Servers. Via Socket ist performant, hat allerdings den Nachteil, das der Socket eine Begrenzung hat und daher in größeren Umgebungen versagen kann. Die lokale IP 127.0.0.1 ist dann Sinnvoll, wenn sich der Webserver ebenfalls lokal auf dem Server befindet, man aber kein Socket einsetzen kann. Die Anbindung an die öffentliche IP des Servers ergibt immer dann Sinn, wenn vorgelagerte Webserver Jobs an den PHP-Server übergeben.


Socket

listen = /var/run/php7-fpm-<poolname>.sock
listen.owner = www-data
listen.group = www-data

Lokale IP

listen = 127.0.0.1:9001
listen.allowed_clients = 127.0.0.1

Öffentliche IP

listen = 10.0.1.10:9002
listen.allowed_clients = 10.0.1.1, 10.0.1.2, 10.0.1.3

Prozesse

php-fpm bietet uns über den Prozessmanager die Möglichkeit die Prozesse unterschiedlich zu steuern. Wir können festlegen, ob oder wie viele PHP-Prozesse initial gestartet werden sollen, Wie viele PHP-Prozesse mindestens und wie viele es maximal sein dürfen. Es gibt dabei drei unterschiedliche Modis:


Static

Diese Einstellung ist für hoch frequentierte PHP-Server interessant, da die konfigurierten Ressourcen immer gestartet bereitstehen.

pm = static
pm.max_children = 15
pm.start_servers = 3
pm.max_requests = 500

Ondemand

Wer auf seiner Seite nur fünf Besucher am Tag hat, wie diese Seite :), kann PHP-Prozesse erst bei Anforderung starten lassen. Das schont zwar die Ressourcen, ist aber bei initialen Verbindungen immer etwas träge.

pm = ondemand
pm.max_children = 15
pm.process_idle_timeout = 10s
pm.max_requests = 200

Dynamic

Das ist die typische Konfiguration, da eine Mindestanzahl an PHP-Prozessen vorgehalten werden, aber bei Last neue Prozesse hinzugefügt werden.

pm = dynamic
pm.max_children = 15
pm.start_servers = 3
pm.min_spare_servers = 3
pm.max_spare_servers = 5
pm.process_idle_timeout = 10s

Anzahl der Prozesse berechnen

Wie viele Prozesse der Server starten kann, hängt sehr stark von der Anwendung und der Leistung des Servers ab. Als Richtwert gilt immer der durchschnittliche Speicherbedarf der PHP Prozesse und der verfügbare Arbeitsspeicher

Maximale Prozesse = Freier Arbeitspeicher / Speicherbedarf der Prozesse

Der durchschnittliche Bedarf eines PHP Prozesses an Arbeitsspeicher kann man mit dem folgenden Befehl ermitteln. Es ist natürlich immer nur eine Momentaufnahme und sollte über einen längeren Zeitraum ermitelt werden. Bitte beachtet, dass der Prozessname im folgenden Aufruf (php-fpm) unter umständen anders lautet - ändert den Namen des Prozesses entsprechend ab.

ps --no-headers -o "rss,cmd" -C php-fpm | awk '{ sum+=$1 } END { printf ("%d%s\n", sum/NR/1024,"M") }'

PHP Werte

Ich hatte eingangs erwähnt, das man Werte aus der globalen php.ini in der Pool-Konfiguration überschreiben kann. Dazu kann man via php_admin_value neue Werte setzen, in diesem Fall wären es Werte für unseren PHPMyAdmin Pool

php_admin_value[open_basedir] = /tmp:/usr/share/php:/var/lib/php/sessions:/var/www/pma
php_admin_value[upload_max_filesize] = 200M
php_admin_value[memory_limit] = 512M
php_admin_value[max_execution_time] = 600
php_admin_value[max_input_time] = 300

Webserver anbinden

Mit einem Webserver haben wir nun die Möglichkeit einen PHP-Server via Socket anzubinden oder aber über den fcgi Proxy. Der Proxy ist nur dann Sinnvoll, wenn der Webserver nicht lokal auf dem PHP-Server ist oder der Socket mit der angeforderten Leistung nicht mithalten kann. Die folgenden Einträge müssen in die entsprechende Virtualhost eingetragen werden.


Apache

Socket

<FilesMatch \.php$>
  SetHandler "proxy:unix:/var/run/php7-fpm.sock|fcgi://localhost/"
</FilesMatch>

IP

<FilesMatch \.php$>
   SetHandler "proxy:fcgi://127.0.0.1:9000"
</FilesMatch>

Nginx

Socket

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/var/run/php7-fpm.sock;
}

IP

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass 127.0.0.1:9000;
}