PHP Hilfe: Load Average zu hoch – Load Balance in PHP-Scripte integrieren

Wer mit PHP serverseitig große Datenmengen verarbeitet wird das Problem kennen: Die Load Average steigt hoch und es beginnen Tasks in der CPU-Warteschleife anzustehen. Das ist schlecht für die Performance und der Einstieg in einen Teufelskreis, der den Server komplett überlasten kann. Er liefert dann beispielsweise keine Webseiten und Emails mehr aus oder reagiert nicht mehr. Deshalb sollten arbeitsintensive PHP-Scripte diesen Wert auslesen und die Arbeitslast entsprechend reduzieren wenn die Server-Hardware an ihre Grenzen kommt.

Was ist die Load Average?

Das ist ein adslesbarer Wert aus der Systeminformation bei Linux-Servern. Das Äquivalent dazu bei Windows-Servern wäre die “Process Queue Length”.

Ich werde hier aber nur auf die Linux-Version eingehen. Wer mit einem Windows-Server Probleme hat mit Hardware-Überlastung, dem würde ich erst einmal einen Umstieg auf einen Linux-Server raten. Und ein gutes Buch dazu.

In der Shell kann man die Werte anzeigen, indem man “uptime” ausführt:

:~# uptime
16:05:21 up 24 days, 19:17,  1 user,  load average: 3,23, 2,75, 2,60

Hier wird angezeigt wie lange der Server schon läuft, wie viele Benutzer eingeloggt sind und die drei Werte der Load Average. Diese sind die Mittelwerte der letzten Minute, der letzten fünf Minuten und der letzten 15 Minuten.

Ohne weitere Systeminformationen sagen die Werte wenig aus. Bei einem Systemprozessor würde ein Wert von 1,0 bedeuten, dass er gerade so alle anstehenden Prozesse bearbeiten kann und kein Prozess in der Warteschleife ansteht. Mit vier Systemprozessoren wäre das bei einem Wert von 4,0 der Fall.

Mit einer höheren Anzahl an Prozessoren sind höhere Werte durchaus noch gesund. Mein Octacore (8) ist mit einer Load Average von 3,23 weniger als 50% ausgelastet. Obwohl im Moment gerade eine csv-Datei mit über 4 Millionen Produktdaten aufwändig bearbeitet und in eine Datenbank geschrieben wird. Außerdem ist auf dem Server noch ein Apache Webserver ziemlich beschäftigt. Jeder einzelne Datensatz wird in einer Schleife durchlaufen und dabei mit ressourcenintensiven String-Operationen, Variablen-Zuweisungen, Array-Operationen, Berechnungen, Datenbank-Lesezugriffen und Datenbank-Schreibzugriffen bearbeitet.

Die Load Average ist kein rein CPU-abhängiger Wert. Er sagt nur aus, dass Prozesse zur Verarbeitung an der CPU anstehen. Die Gründe dafür können vielseitig im System liegen. Wenn beispielsweise die Lese- und Schreibzugriffe auf die Festplatte(n) oder in das Netzwerk nicht hinterher kommen, dann kann die CPU auch nicht weiter machen. Die Load Average-Werte können also hoch sein obwohl die CPU(s) gar nicht wirklich viel zu tun haben.

Load Average einfach erklärt

Stelle Dir vor: Du stehst an einer Brücke und sollst die Verkehrslage überwachen und melden. Manchmal kommt so viel Verkehr zu der Brücke, dass sich die Autos stauen bevor sie auf die Brücke fahren können. Du möchtest die Autofahrer wissen lassen wie der Verkehr auf Deiner Brücke fließt. Ein ausgezeichneter Wert hierfür wäre:

Wie viele Autos warten gerade vor der Brücke?

Wenn keine Autos warten kann man direkt durchfahren, wenn Autos an der Brücke anstehen müssen sich die Fahrer auf eine Verspätung einstellen.

Also, Brücken-Operator, … welches Zahlensystem würdest Du benutzen?

Wie wäre es mit:

  • 0,00 bedeutet überhaupt kein Verkehr. Eigentlich bedeutet ein Wert zwischen 0,00 und 1,00, dass die Autos ohne Wartezeit direkt durchfahren können.
  • 1,00 bedeutet, die Brücke ist zu 100% ausgelastet. Es ist immer noch alles gut, aber es ist viel Verkehr und er fängt an etwas langsamer zu fließen.
  • Über 1,00 bedeutet Stau. Wie viel Stau? 2,00 bedeutet, dass eine Spur auf der Brücke ausgelastet ist und Autos, die auf eine komplette Brückenspur passen würden im Stau stehen. Usw…

Du sagst also, eine ideale Load Average wäre 1,00 und alles darunter?

Nicht wirklich. Mit 1,00 ist kein Platz nach oben. In der Praxis ziehen viele Sysadmins die Grenze bei 0,70 für einen Prozessor.

  • Die “da muss ich mal danach schauen” – Faustregel: 0,70. Wenn die Load Average dauerhaft über 0,70 liegt, sollte genauer untersucht werden woran es liegt bevor alles wirklich schlecht wird.
  • Die “jetzt sofort darum kümmern” – Faustregel: 1,00. Wenn die Load Average dauerhaft über 1,00 liegt, dann finde das Problem jetzt sofort und löse es!
  • Die “aargh, es ist 3:00 Uhr Nachts. WTF?!?!” – Faustregel 5,00. Wenn die Load Average über 5,00 steigt dann hast Du ein echtes Problem. Der Server wird hängen oder sehr langsam reagieren. Und das passiert natürlich zur möglichst unpassendsten Zeit: mitten in der Nacht oder wenn Du gerade anderweitig beschäftigt bist.

Die Lösung: effizientere Scripte, Load Balance oder mit Hardware nach dem Problem werfen

Hardware ist heutzutage nicht mehr teuer und oft ist die Standard-Lösung: schnellere Hardware. Das funktioniert eigentlich immer aber es ist nicht effizient, nicht elegant und schon gar nicht ressourcensparend. Und trotzdem manchmal angebracht.

Auch beim Programmieren kann man eine Menge Fehler machen. Obwohl erst einmal alles funktioniert, kann ein ineffizienter Quellcode enorm viel Rechenleistung verbrauchen. Bei meinen > 4 Millionen Datensätzen sind derzeit Millisekunden entscheidend weil beispielsweise eine Vielzahl von String-Operationen in jeder Schleife über 30x ausgeführt werden.

Um zwei Beispiele zu nennen:

Berechne nur ein Mal

Eine einfache for-Schleife, die so lange ausgeführt wird bis $i größer ist als die Anzahl der Elemente im Array $arr:

for( $i=0; $i > count($arr); $i++){
  echo count($arr);
}

Das Problem oben ist: bei jedem Schleifendurchgang wird das Array $arr zwei Mal neu gezählt. So etwas sieht man sehr oft und ist völliger Blödsinn. Diese Schleife mit den 4.000.000 Datensätzen und allen zusätzlichen Operationen laufen zu lassen würde bedeuten: der Server ist dieses Jahr etwa zu Weihnachten damit fertig.

Besser so:

$laenge = count($arr);
for( $i=0; $i < $laenge; $i++){
  echo $laenge;
}

Das Array wird ein Mal gezählt und der Wert in der Variable $laenge gespeichert.

String-Zuweisungen in einfachen Anführungsstrichen

$b = "ist aber bullshit."
$a = "Mein Code funktioniert, $b";
echo $a;
// Ausgabe:
// Mein Code funktioniert, ist aber bullshit.

Einen String einer Variable zuzuweisen mit doppelten Anführungsstrichen kostet Rechenzeit weil der PHP-Interpreter in einem String mit doppelten Anführungsstrichen nach Variablen sucht. Das sind einzeln in etwa 5 Millisekunden, aber:

4.000.000 * 30 String-Zuweisungen/Schleife * 5 Millisekunden = 600.000.000 Millisekunden
(600 Millionen Millisekunden – oder 600.000 Sekunden oder 10.000 Minuten, oder 166,67 Stunden – reine Prozessor-Rechenzeit)

Und da sieht man direkt die Dimension des Ganzen. Es geht hier erst mal gar nicht um die Zeit, die der Task bis zur Fertigstellung benötigt. Sondern darum, die Prozessorauslastung nicht durch die Decke gehen zu lassen damit der Server seine anderen Aufgaben auch erledigen kann.

Besser so:

$b = 'und ich bin nicht der letzte N00B.'
$a = 'Mein Code funktioniert '.$b;
echo $a;
// Ausgabe:
// Mein Code funktioniert und ich bin nicht der letzte N00B.

Das Thema “Microoptimierung” von PHP ist unendlich. Ich schreibe da irgendwann einen extra Beitrag dazu mit realen Benchmarks.

Was kann ich tun wenn die Load Average zu hoch und mein Code optimiert ist?

Nun, ganz einfach. Oder auch nicht. Es hängt davon ab wie schnell man den Task erledigt haben will / muss. Entweder, wie oben erwähnt, mit Hardware nach dem Problem werfen – oder die CPU-Auslastung ausbalancieren, so dass der Server noch reaktionsfähig bleibt.

Letzteres geht am Einfachsten, indem man die Load Average mit PHP ausliest und die Bearbeitung des Scripts für eine kurze Zeit schlafen lässt mit sleep():

$loadAverage = sys_getloadavg();
if ($loadAverage[0] > 0.70) {
  sleep(2);
}

sys_getloadavg() gibt ein Array mit drei Werten zurück:

  • $loadAverage[0] – der Mittelwert der letzten Minute
  • $loadAverage[1] – der Mittelwert der letzten fünf Minuten
  • $loadAverage[2] – der Mittelwert der letzten fünfzehn Minuten

Falls die Load Average über 0,7 liegt, schläft die Abarbeitung des Prozesses für zwei Sekunden um Ressourcen frei zu halten. Die Werte können individuell angepasst werden.