Der diesjährige Adventskalender wurde mit einem Türchen zum DAMPF-Stack eröffnet. Dahinter versteckte sich nichts weiter als der altbekannte LAMP-Stack bestehend aus Linux, dem Apache HTTP Server, MariaDB und PHP. Das L wurde zu D wie Debian konkretisiert, und PHP wurde um FPM ergänzt: den FastCGI Process Manager.
Dabei kam von uname folgende Frage auf:
Das ist ein berechtigtes Anliegen! Doch wie soll das ganze überprüft werden? Mir erscheint folgende Testanordnung sinnvoll:Ich würde nun gerne wissen, wie viel schneller DAMPF im Gegensatz zu DAMP ist. Kann vielleicht jemand ein Script schreiben, wo über einen Webservice eine Vielzahl an Anfragen einmal “langsam” mit mod_php und einmal mit Dampf mit PHP-FPM durchgeführt wird?
- Man benötigt ein serverseitiges Skript, das eine mehr oder weniger hohe Last erzeugt.
- Weiter muss ein Client simuliert werden, der einerseits viele und/oder “schwere” Anfragen absetzt, die serverseitig eine hohe Last generieren. Ausserdem muss dieser Client sinnvolle Statistiken über die Anfragedauern ausgeben können.
Serverseitiges Skript: Primzahlfaktorisierung
Jede natürliche Zahl >= 2 kann als ein Produkt von Primzahlen ausgedrückt werden:
- 12 = 2 * 2 * 3
- 13 = 13
- 15 = 3 * 5
- 27 = 3 * 3 * 3
- Man findet die Primzahlen bis x (bzw. zur Quadratwurzel von x als Optimierung, die hier nicht weiter begründet werden soll).
- Man versucht die Zahl x durch die Primzahlen in aufsteigender Reihenfolge zu dividieren.
- Funktioniert die Division restlos, hat man einen neuen Primfaktor gefunden. Man fährt mit dem Rest und der gleichen Primzahl fort.
- Andernfalls versucht man die Division mit der nächsten Primzahl.
- Der Vorgang ist fertig, wenn entweder der Rest bei 1 angelangt ist, oder wenn die Primzahlen durchprobiert worden sind: In diesem Fall ist der Rest auch eine Primzahl und somit ein Primfaktor von x.
Primzahlen können folgendermassen gefunden werden:
Code: Alles auswählen
function primes_up_to($n) {
$primes = array();
if ($n < 2) {
return $primes;
}
for ($i = 2; $i <= $n; $i++) {
if (is_prime($i)) {
$primes[] = $i;
}
}
return $primes;
}
function is_prime($x) {
for ($i = 2; $i <= $x / 2; $i++) {
if ($x % $i == 0) {
return false;
}
}
return true;
}
Diese Implementierung ist ineffizient ‒ und somit für einen Lasttest ideal.
Die Faktorisierung einer einzelnen Zahl x funktioniert folgendermassen:
Code: Alles auswählen
function factorize($x) {
$factors = array();
$primes = primes_up_to(sqrt($x));
$n = count($primes);
for ($i = 0; $x > 1 && $i < $n;) {
$prime = $primes[$i];
if ($x % $prime == 0) {
$factors[] = $prime;
$x /= $prime;
} else {
$i++;
}
}
if ($x > 1) {
$factors[] = $x;
}
return $factors;
}
Code: Alles auswählen
function factorize_range($a, $b) {
$factors = array();
if ($a > $b) {
return $factors;
}
for ($i = $a; $i <= $b; $i++) {
$factors[$i] = factorize($i);
}
return $factors;
}
Das PHP-Skript soll Benutzeranfragen entgegennehmen und im Klartext beantworten:
Code: Alles auswählen
header("Content-Type: text/plain");
if (!array_key_exists("lower", $_GET) || !array_key_exists("upper", $_GET)) {
die("usage: ?lower=[lower]&upper=[upper]");
}
$result = factorize_range($_GET["lower"], $_GET["upper"]);
foreach ($result as $n => $fs) {
echo("{$n}:\t");
foreach ($fs as $f) {
echo("{$f} ");
}
echo("\n");
}
Es wird also mit den GET-Parametern lower und upper aufgerufen. Auf unserem DAMPF-Server als prime_factorization.php hinterlegt, kann es folgendermassen aufgerufen werden:
Code: Alles auswählen
$ curl 'http://localhost/prime_factorization.php?lower=100&upper=109'
100: 2 2 5 5
101: 101
102: 2 3 17
103: 103
104: 2 2 2 13
105: 3 5 7
106: 2 53
107: 107
108: 2 2 3 3 3
109: 109
Clientseitig Last generieren: Der request0r
Um Performanceunterschiede zwischen mod_php und PHP-FPM ermitteln zu können, müssen mehrere Requests gleichzeitig abgesetzt werden. Sowas liesse sich gut mit einem Shell-Skript, dem curl-Befehl und dem Operator & umsetzen, womit Prozesse im Hintergrund ausgeführt werden können. Das Auswerten der einzelnen Laufzeiten wird aber damit eher umständlich.
Ein kleines Go-Programm namens request0r soll hier Abhilfe schaffen. Das Projekt ist auf GitHub zu finden. Da es im Wesentlichen aus einer einzigen Quellcodedatei besteht, kann es aber auch via NoPaste heruntergeladen werden.
Das Programm lässt sich mit Go bauen und folgendermassen ausführen:
Code: Alles auswählen
$ go build request0r.go
$ ./request0r -w 2 -r 10 'http://localhost/prime_factorization.php?lower=100&upper=109'
Requests:
Total Passed Failed Mean
20 20 0 1.183614ms
Percentiles:
0% 25% 50% 75% 100%
829.967µs 950.424µs 1.082723ms 1.368022ms 1.693025ms
Als Statistik wird einerseits ausgegeben, wie viele Requests insgesamt abgesetzt worden sind (Total: Anzahl Worker mit der Anzahl Requests pro Worker multipliziert) und wie viele Requests davon erfolgreich zurückkamen (Passed) bzw. gescheitert sind (Failed).
Das arithmetische Mittel der Antwortzeiten wird als Mean ausgewiesen, welches einen guten Indikator für die Performance darstellt. Ein differenzierteres Bild ergibt der Blick auf die Perzentile: Der schnellste Request (0%), der langsamste (100%) sowie diejenigen auf verschiedenen Schwellen (25%, 50% ‒ der Median, 75%) werden ebenfalls ausgewiesen, womit sich Ausreisser besser erkennen lassen.
Im obigen Beispiel kamen die 25% der schnellsten Anfragen in weniger als einer Millisekunde zurück, während die längste Anfrage fast 1.7 Millisekunden auf sich warten liess. Weil das arithmetische Mittel mit 1.18 Millisekunden über dem Median von 1.08 Millisekunden liegt, gibt es offenbar stärkere Ausreisser nach oben (d.h. langsamere) als nach unten (d.h. schnellere).
Client und Server wären also bereit um mal ordentlich DAMPF im Kessel zu machen!
Wechsel zwischen mod_php und PHP-FPM
Für die Lasttests soll einfach zwischen mod_php und PHP-FPM hin- und hergewechselt werden können. Hierzu wird ein kleines Skript namens toggle-fpm.sh zur Verügung gestellt (siehe NoPaste). Mit ./toggle-fpm.sh enable wird PHP-FPM aktiviert; mit ./toggle-fpm.sh disbable wird es deaktiviert und mod_php aktiviert. Die derzeitig aktive PHP-Implementierung lässt sich via phpinfo() in Erfahrung bringen.
So soll PHP-FPM deaktiviert werden, um etwas Last unter mod_php zu erzeugen:
Code: Alles auswählen
./toggle-fpm.sh disable
Code: Alles auswählen
Server API: Apache 2.0 Handler
Es sollen zwei Testreihen ausgeführt werden:
- viele kurze Requests
- Faktorisierung von 100 bis 999
- vier Worker mit je 250 Requests
- wenige lange Requests
- Faktorisierung von 10^9 bis (10^9)+10
- vier Worker mit je zehn Requests
Code: Alles auswählen
$ ./request0r -w 4 -r 250 'http://localhost/prime_factorization.php?lower=100&upper=999'
Requests:
Total Passed Failed Mean
1000 1000 0 8.236957ms
Percentiles:
0% 25% 50% 75% 100%
3.032697ms 4.006084ms 6.557438ms 10.872339ms 36.512993ms
$ ./request0r -w 4 -r 10 'http://localhost/prime_factorization.php?lower=1000000000&upper=1000000010'
Requests:
Total Passed Failed Mean
40 40 0 10.35933729s
Percentiles:
0% 25% 50% 75% 100%
9.231410123s 10.165946763s 10.259091202s 10.383718102s 11.504672585s
Code: Alles auswählen
$ ./request0r -w 4 -r 250 'http://localhost/prime_factorization.php?lower=100&upper=999'
Requests:
Total Passed Failed Mean
1000 1000 0 7.723427ms
Percentiles:
0% 25% 50% 75% 100%
2.944284ms 5.398297ms 7.420241ms 9.426545ms 25.895501ms
$ ./request0r -w 4 -r 10 'http://localhost/prime_factorization.php?lower=1000000000&upper=1000000010'
Requests:
Total Passed Failed Mean
40 40 0 9.771434526s
Percentiles:
0% 25% 50% 75% 100%
9.378479225s 9.643530472s 9.724563396s 9.8192836s 10.709784636s
Übungen
Nun stellen sich zwei Fragen:
- Sind diese Messungen überhaupt statistisch relevant?
- Liesse sich PHP-FPM nicht noch etwas tunen?
- Anhand des DAMPF-Setups vom 1. Dezember und der hier vorliegenden Anleitung soll das Setup nachgebaut und getestet werden. Erscheinen vergleichbare und v.a. wiederholbare Messresultate? Mit welchen Parametern (Anzahl Worker, Anzahl Requests, Unter- und Obergrenze der Faktorisierung pro Request) erhält man welche Zeiten?
- Unter /etc/php/8.2/fpm/pool.d/www.conf liesse sich der Prozess-Pool für PHP-FPM umkonfigurieren. Standardmässig wird ein dynamischer Prozesspool verwendet (pm = dynamic). Interessante Optionen wären pm.max_children, pm.start_servers, pm.min_spare_servers und pm.max_spare_servers, womit sich die Anzahl PHP-Prozesse steuern lässt. Weitere Direktiven mit dem Präfix pm könnten auch eine Einfluss auf die Performance haben.