Webanwendung auf Windows Server, Apache 2.4 mit mod_php, PHP 7.4.9 auf 8.2.35
Branche: Internetportale · Dauer: 4 Stunden
Ausgangslage
Die Startseite der Anwendung wird in PHP erzeugt; danach ruft der Browser parallel rund 20 AJAX-Skripte auf. Alle Skripte binden eine gemeinsame Startdatei ein, die Konfiguration, Autoloader und Template-Engine lädt, session_start() aufruft und einen Wert in $_SESSION schreibt.
Nach dem Update von PHP meldete das Team des Kunden, die Anwendung sei „ungleichmäßig langsamer“ geworden. Im Verdacht standen ein allgemein langsameres PHP 8 und die Sitzungen. Ein session_write_close() in einem der AJAX-Skripte half, aber niemand wusste, warum, und ob PHP 8 das Verhalten der Sitzungen geändert hatte. Die Frage lautete: Lässt sich das beheben, ohne die Anwendung umzuschreiben?
Lösung
Wir haben die Anwendung in zwei Docker-Containern nachgestellt, php:7.4-apache und php:8.2-apache, also Apache mit mod_php wie beim Kunden, mit denselben Dateien und der echten Template-Engine bei jedem Aufruf. Ein Lastgenerator verhielt sich wie ein Browser: eine angemeldete Sitzung, in jeder Runde 21 parallele Aufrufe mit demselben Cookie, die Startseite und 20 AJAX-Aufrufe. Dazu kam ein Dauertest, bei dem sechs Clients ohne Pause AJAX-Aufrufe senden und alle zwei Sekunden die Startseite aufgerufen wird.
Beide Versionen verhielten sich identisch, ein Rückschritt von PHP 8 war es nicht. Der Standard-Handler files setzt in session_start() eine exklusive, blockierende Sperre auf die Sitzungsdatei und hält sie bis session_write_close() oder bis zum Ende des Skripts. Alle Aufrufe eines Nutzers laufen damit nacheinander: Zwanzig Aufrufe zu je 1,5 Sekunden sind 30 Sekunden für den letzten, und eine in dieser Zeit geklickte Startseite steht in derselben Schlange. Unter Windows zählt max_execution_time die echte Zeit, also verbraucht schon das Warten auf die Sperre das Limit und endet im fatalen Fehler.
Die Lösung ist ein Schalter in der gemeinsamen Startdatei: Ein Skript, das die Sitzung nur liest, gibt die Sperre sofort nach dem Lesen frei. Eine Zeile pro Skript, ohne Umbau der Anwendung.
Ergebnisse
- 20 parallele AJAX-Aufrufe zu je 1,5 s, Zeit des letzten
- Von 30,0 s auf 1,5 s
- Wartezeit der Startseite während der parallelen Aufrufe
- Von 1,6 bis 1,8 s auf 0,00 s
- Änderung am Code
- Ein Schalter in der Startdatei, eine Zeile pro Skript
- Verhalten der Sitzungssperre
- PHP 7.4 und PHP 8.2 gleich