Nach dem Update auf PHP 8.2 brachen Seiten zufällig nach 30 Sekunden ab. Schuld war die Sperre der Sitzungsdatei, nicht PHP 8

Nach dem Update von PHP 7.4 auf PHP 8.2 lud dieselbe Seite einmal normal und brach einmal nach 30 Sekunden ab. Wir haben beide Versionen nebeneinander nachgestellt und die Ursache gefunden: Rund 20 parallele AJAX-Aufrufe eines Nutzers warteten nacheinander auf die Sperre der Sitzungsdatei. Behoben mit einer Zeile pro Skript, ohne die Anwendung umzuschreiben.

  • 20 parallele AJAX-Aufrufe: 30 s, danach 1,5 s
  • Wartezeit der Startseite: 1,6 s, danach 0 s
  • Eine Zeile pro Skript statt einer neuen Anwendung
Oben unter session_start() zwanzig schwarze Balken als Treppe, einer nach dem anderen, bis zur Marke 30 s; unten unter + session_write_close() dieselben zwanzig Balken in Orange nebeneinander, alle fertig bei 1,5 s

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

So sind wir vorgegangen

  1. Das Problem kontrolliert nachgestellt

    Zwei Container, PHP 7.4 und PHP 8.2 mit Apache und mod_php wie beim Kunden, dieselben Dateien und die echte Template-Engine bei jedem Aufruf.

  2. Last wie aus einem Browser

    Eine angemeldete Sitzung, 21 parallele Aufrufe mit demselben Cookie, die Zeit jedes Aufrufs gemessen; die Arbeitszeit eines Aufrufs per Parameter einstellbar.

  3. Ein Dauertest aus Sicht des Nutzers

    Sechs Clients senden ohne Pause AJAX-Aufrufe, alle zwei Sekunden die Startseite; gemessen wird, wie lange die Startseite wartet.

  4. Die Quellen von PHP verglichen

    Das Sitzungsmodul, die Nachbildung von flock() unter Windows, die Umsetzung des Zeitlimits und die UPGRADING-Hinweise aller Versionen 8.x.

  5. Nebenhypothesen geprüft

    Opcache, Garbage Collection der Sitzungen, das Verhalten von session_write_close() und eine Sitzung ohne Sperre.

Gemessen in beiden Versionen von PHP

Variante PHP 7.4 PHP 8.2
20 AJAX-Aufrufe zu je 1,5 s, ohne Freigabe der Sperre 30,0 s 30,0 s
Dasselbe mit session_write_close() direkt nach dem Lesen der Sitzung 1,5 s 1,5 s
Dasselbe mit session_start() und der Option read_and_close 1,5 s 1,5 s
Dasselbe mit einem Sitzungs-Handler ohne Sperre 1,5 s 1,5 s
Dauertest: Wartezeit der Startseite, Sperre bis zum Ende des Skripts 1,6 bis 1,8 s 1,5 bis 1,6 s
Dauertest: Wartezeit der Startseite, Sperre früh freigegeben 0,00 s 0,00 s

Herausforderungen, die wir gelöst haben

Unter Linux lässt sich der Fehler nicht nachstellen

Unter Linux zählt max_execution_time die Rechenzeit des Skripts, und das Warten auf eine Sperre verbraucht keine. Unter Windows ist die gemessene Zeit die echte Zeit, so steht es auch im PHP-Handbuch. Deshalb haben wir unter Linux die echte Dauer der Aufrufe gemessen und alles über 30 Sekunden als Abbruch in der Produktion gewertet.

PHP 8 hat die Sperre nicht geändert, PHP 7.4.22 aber flock() unter Windows

Die Korrektur des Bugs #81223 (öffnet in einem neuen Fenster) vom Juli 2021 sperrt unter Windows die ganze Datei statt des ersten Bytes. Der Kunde kam von PHP 7.4.9, für ihn war das ein echter Unterschied, nur nicht durch PHP 8. Weil Sperren unter Windows verbindlich sind, stört seitdem jeder andere Prozess, der Sitzungsdateien liest, etwa ein Virenscanner oder ein Indexdienst. Den Ordner der Sitzungen nehmen wir deshalb vom Scannen aus.

Ungleichmäßig, weil die Reihenfolge des Aufweckens offen ist

Wer auf die Sperre wartet, kommt nicht in der Reihenfolge der Anfragen dran. Bei identischer Last wartete die Startseite einmal 0 und einmal 4,5 Sekunden. Daher der Eindruck „mal geht es, mal nicht“.

Die Garbage Collection der Sitzungen läuft unter der Sperre

Sie startet zufällig in session_start(), nach dem Setzen der Sperre, und durchsucht den ganzen Ordner der Sitzungen. Bei 100 000 Dateien dauerte das unter Linux 0,3 Sekunden, auf NTFS mit Virenscanner deutlich länger. Ohne php.ini gilt eine Wahrscheinlichkeit von 1 zu 100 je Aufruf, nicht 1 zu 1000 wie in php.ini-production; bei 20 Aufrufen je Seite trifft das einen spürbaren Teil der Seitenaufrufe.

session_write_close() ändert die Regeln, ohne Fehlermeldung

Ein Schreiben in $_SESSION nach dem Schließen meldet keinen Fehler und speichert nichts. Ein erneutes session_start() wartet wieder auf die Sperre und ersetzt $_SESSION durch den Inhalt der Datei, Werte dazwischen gehen verloren. Nach gesendeten Headern scheitert der Neustart mit einer Warnung. Jedes dieser Verhalten haben wir in beiden Versionen bestätigt.

Eine Sitzung ohne Sperre ist keine Lösung

Ein Handler ohne Sperre ist möglich, kostet aber Schreibzugriffe: Im Test überschrieb ein langsamerer Aufruf mit seiner veralteten Kopie die Daten, die ein schnellerer parallel gespeichert hatte. In einer Anwendung, die bei jedem Aufruf in die Sitzung schreibt, wären das zufällige Abmeldungen und verschwundene Einstellungen. Auch Opcache beschleunigt nur: Ein Aufruf brauchte 0,5 statt 1,9 ms, die Sperre blieb bei 30 Sekunden.

Die Lösung, ein Schalter in der Startdatei

Statt session_write_close() in jedes AJAX-Skript einzeln zu schreiben, bekam die gemeinsame Startdatei einen Schalter. Ein Skript, das die Sitzung nur liest, erklärt das vor dem Einbinden:

define('SESSION_READONLY', true);

Die Startdatei macht das Bisherige und gibt die Sperre gleich nach dem Lesen frei:

$bSessionReadOnly = defined('SESSION_READONLY') && SESSION_READONLY;

session_start();
$_SESSION['marker'] = 0;

if ($bSessionReadOnly) {
    session_write_close();
}

$_SESSION bleibt bis zum Ende lesbar, die Sperre dauert Bruchteile einer Millisekunde. Muss ein solches Skript selten doch schreiben, gilt: erst rechnen, dann ein kurzes Fenster öffnen, schreiben, schließen, vor jeder Ausgabe.

Dazu kamen Einstellungen ohne Änderung am Code: Opcache mit file_cache als Rückfall unter Windows, ein eigener Ordner für Sitzungen ohne Virenscan, mit Kontrolle der Dateizahl und ausdrücklich gesetztem session.gc_divisor. Der Kunde erhielt ein Messwerkzeug in PHP auf Basis von curl_multi, das auf dem Windows-Server einen einzelnen Aufruf und 21 parallele Aufrufe vor und nach der Änderung misst: Dauern die parallelen etwa 20-mal so lange wie einer, laufen sie nacheinander.

Was wir daraus mitnehmen

  • „Nach dem Update“ heißt nicht Rückschritt. Hier hat das Update eine Schwäche sichtbar gemacht, die seit Jahren bestand.
  • Zwei Versionen nebeneinander unter gleicher Last klären den Streit um die Ursache schneller als jede Diskussion.
  • Im Zweifel die Quellen lesen. Einige Dutzend Zeilen in mod_files.c und flock_compat.c haben die Frage „wartet PHP 8 auf die Sitzung?“ beantwortet.
  • Plattformen unterscheiden sich. Derselbe Code bricht unter Windows ab und läuft unter Linux, nur wegen der Definition des Zeitlimits.
  • Die beste Korrektur ändert am wenigsten. Ein Schalter dort, wo das Team bei Problemen mit Sitzungen ohnehin nachsieht, statt einer neuen Anwendung oder Sitzungen ohne Sperre auf Kosten der Datenkonsistenz.

Quellen

Verwandte Ausgangslagen

  • Für PHP 8.2 enden die Sicherheits-Updates am 31.12.2026, für PHP 8.1 und älter sind sie bereits beendet (Stand: 2026-10). Wir bringen gewachsene PHP-Anwendungen, auch mit Smarty-Templates und jQuery-Frontend, auf PHP 8.4 oder 8.5 und testen jede Stufe.

  • Ihr System läuft, aber niemand kennt es genau. Wir erfassen Anwendung, Datenbank, Server und E-Mail, bewerten Risiken, empfehlen schriftlich den weiteren Weg und übernehmen auf Wunsch den Betrieb.

Weitere Projekte

  • Ein Browser, ein Kasten mit der Beschriftung Apache und drei Bahnen zu PHP, PHP und CFML; die CFML-Bahn und ihr Kasten sind orange

    ColdFusion 2025 im Produktivbetrieb hinter Apache: fünf nicht offensichtliche Fallen

    Wir haben eine Produktivinstallation von Adobe ColdFusion 2025 auf Debian 12 vorbereitet, hinter einem Apache 2.4, der schon mehrere andere Sites bedient. Die Installation selbst dauert 34 Sekunden. Die Aufmerksamkeit ging in den Konnektor und in Details, die leicht übersehen werden: ein Fehler 503 durch IPv6, Zuordnungen in jedem virtuellen Host und ein Profil, das still einen Debugger hinzufügt.

  • Vier Briefumschläge passieren die Tore SPF, DKIM und DMARC, jeweils orange abgehakt, auf dem Weg in den Posteingang

    E-Mail-Zustellbarkeit für vier Firmendomains

    Ein Mailserver für vier Firmendomains signierte nichts, veröffentlichte keine DMARC-Richtlinie und wurde von Microsoft abgewiesen. Unsere Ingenieure haben ihn geprüft, die Änderung an einer Domain erprobt und in Gruppen ausgerollt. Das Ergebnis: Alle drei Prüfungen bestehen bei Gmail, die Microsoft-Sperre ist aufgehoben, und die Nutzer haben nichts bemerkt.

In 20 Minuten klären wir, welcher erste Schritt zu Ihrem System passt

Sie schildern Ziel und Stand, wir sagen offen, ob und wie wir helfen können. Antwort innerhalb eines Werktags, unverbindlich.

Telefon (Deutsch, Polnisch, Englisch) +48 501 250 220
E-Mail E-Mail

Erreichbar: Montag bis Freitag, 9:00 bis 17:00 Uhr (MEZ); Support und Notfälle rund um die Uhr (24/7)