Die Frage kommt meist beim ersten Lastproblem: Können wir nicht einfach einen zweiten Server dazustellen? Die Antwort ist fast immer nein, und der Grund ist nie die Anwendungslogik. Es sind drei Dinge, die im Lauf der Jahre auf der Festplatte des einen Servers gelandet sind und dort nicht weg wollen.
Solange diese drei Ketten hängen, sind eine ganze Reihe von Dingen unmöglich: mehr als eine Maschine, ein Rollout ohne Unterbrechung, eine Maschine, die ohne Vorwarnung verschwinden darf, und jede Form von automatischer Skalierung. Das ist der Grund, warum diese Arbeit vor einer Cloud-Migration kommt und nicht danach.
Dieser Artikel geht die drei Ketten durch, in der Reihenfolge, in der ich sie löse, und beschreibt für jede den kleinsten Schritt, der wirklich trägt.
Warum die Anwendung nur auf einer Maschine läuft
Eine Anwendung, die auf beliebig vielen Maschinen laufen kann, merkt sich zwischen zwei Anfragen nichts, was nicht in einem geteilten Speicher steht. Alles, was sie sich lokal merkt, bindet den Nutzer an genau die Maschine, die seine letzte Anfrage bearbeitet hat.
In der Praxis wird diese Bindung sichtbar, sobald ein zweiter Server dazukommt: Nutzer werden zufällig abgemeldet, hochgeladene Dateien sind manchmal da und manchmal nicht, und ein Zähler zeigt je nach Server einen anderen Wert. Das Verhalten ist sporadisch und deshalb besonders unangenehm.
Der übliche erste Ausweg heißt klebrige Sitzungen: Der Lastverteiler schickt jeden Nutzer immer zur selben Maschine. Das funktioniert, und es verschiebt das Problem nur. Fällt die Maschine aus, ist der Zustand ihrer Nutzer weg. Ein Rollout auf diese Maschine wirft dieselben Nutzer heraus. Und automatische Skalierung nach unten wird zu einer Entscheidung darüber, wessen Sitzung geopfert wird.
Es lohnt deshalb, die Ketten wirklich zu lösen statt sie zu verwalten. Erfahrungsgemäß sind es Tage, nicht Wochen.
Kette 1: Sitzungen im Dateisystem
PHP legt Sitzungsdaten standardmäßig als Dateien in einem Verzeichnis ab. Das ist der häufigste und der am einfachsten zu lösende Fall.
php -i | grep -E "session.save_handler|session.save_path"
# session.save_handler => files => files
# session.save_path => /var/lib/php/sessions => /var/lib/php/sessionsDie Umstellung auf einen geteilten Speicher ist Konfiguration, kein Code:
session.save_handler = redis
session.save_path = "tcp://redis.intern:6379?auth=...&database=1"
session.gc_maxlifetime = 7200Drei Punkte, die dabei regelmäßig übersehen werden.
Was in der Sitzung liegt, muss serialisierbar sein. In gewachsenen Anwendungen landen dort gelegentlich ganze Objekte, darunter welche mit einer Datenbankverbindung darin. Solange die Sitzung eine Datei auf derselben Maschine war, fiel das nicht auf. Eine Suche nach dem Schreibzugriff auf die Sitzung findet diese Stellen in zehn Minuten.
Die Sitzung wird bei jeder Anfrage geschrieben, auch wenn sich nichts geändert hat. Über das Netz ist das ein zusätzlicher Umlauf je Anfrage. Wo eine Seite die Sitzung nur liest, gehört sie nach dem Lesen geschlossen; das gibt sie frei und spart den Schreibvorgang.
Ein Neustart des Sitzungsspeichers meldet alle ab. Das ist beim Umstellen einmal der Fall und danach bei jedem Ausfall. Wenn das inakzeptabel ist, gehören die Sitzungen in einen Speicher mit Beständigkeit, und das ist eine bewusste Entscheidung mit eigenem Preis.
Der Umstieg selbst braucht kein Wartungsfenster: Neue Sitzungen entstehen im neuen Speicher, alte laufen aus. Wer beides gleichzeitig lesen will, baut dafür mehr, als die Sache wert ist.
Kette 2: Hochgeladene Dateien
Die zweite Kette ist die, die den meisten Code betrifft, und deshalb die, bei der Reihenfolge zählt.
Der Zielzustand ist ein Objektspeicher: Dateien liegen nicht mehr auf der Maschine, sondern in einem Dienst, und die Anwendung spricht über eine Schnittstelle mit ihm. Der Weg dorthin führt über einen Zwischenschritt, der oft übersprungen wird und den ganzen Umbau planbar macht.
Schritt eins: alle Zugriffe an eine Stelle bringen. In einem gewachsenen System stehen move_uploaded_file, file_get_contents und unlink über Dutzende Dateien verteilt. Solange das so ist, ist jede Umstellung ein Dutzend Änderungen mit einem Dutzend Gelegenheiten für Fehler.
grep -rnE "move_uploaded_file|file_put_contents|unlink|fopen" src/ \
--include="*.php" | grep -v "/tests/" | wc -lDahinter kommt eine schmale Schnittstelle, die zunächst genau das tut, was vorher passierte:
interface Dateispeicher
{
public function lege(string $pfad, string $inhalt): void;
public function hole(string $pfad): string;
public function entferne(string $pfad): void;
public function url(string $pfad, int $gueltigSekunden = 300): string;
}Diese Schnittstelle ist die eigentliche Arbeit. Ist sie da, ist der Wechsel des Speichers eine Zeile in der Konfiguration.
Schritt zwei: doppelt schreiben. Die neue Umsetzung schreibt in den Objektspeicher und auf die Platte, gelesen wird weiter von der Platte. Ab jetzt ist alles Neue an beiden Orten.
Schritt drei: den Bestand kopieren, in Portionen, mit einem Abgleich danach. Bei größeren Beständen ist das ein Lauf über Stunden, und er stört nicht, weil niemand darauf wartet.
Schritt vier: den Lesepfad umschalten, mit einem Rückfall auf die Platte, wenn die Datei im Objektspeicher fehlt. Dieser Rückfall bleibt zwei Wochen und wird protokolliert. Was in dieser Zeit protokolliert wird, ist genau die Liste dessen, was beim Kopieren gefehlt hat.
Schritt fünf: Schreiben auf die Platte einstellen, Verzeichnis nach einer Frist entfernen.
Der vierte Schritt ist der, der diesen Umbau von einem Risiko in eine Routine verwandelt, und er ist der, den man in Anleitungen selten findet.
Kette 3: Zustand im Prozess
Die dritte Kette ist die unauffälligste, weil sie in keinem Verzeichnis sichtbar ist. Sie besteht aus Dingen, die sich die Anwendung im Arbeitsspeicher oder in einer lokalen Datei merkt.
Lokale Zwischenspeicher. Ein Zwischenspeicher auf der Platte oder im Opcache ist nicht falsch, solange er nur ableitbare Daten enthält. Er wird zum Problem, wenn er die einzige Quelle für etwas ist, oder wenn zwei Maschinen unterschiedliche Stände haben und der Nutzer je nach Maschine etwas anderes sieht.
Sperren im Prozess. Jede Sperre, die über flock auf eine lokale Datei geht, sperrt nur gegen andere Prozesse derselben Maschine. Auf zwei Maschinen ist sie wirkungslos, und zwar still: Der Code läuft weiter, nur schützt er nichts mehr. Das ist die gefährlichste der drei Ketten, weil ihr Versagen keine Fehlermeldung erzeugt.
grep -rnE "flock|sem_acquire|apcu_|__DIR__ \. '/(cache|tmp)" src/ --include="*.php"Geplante Aufgaben. Ein Cronjob, der auf beiden Maschinen eingerichtet ist, läuft zweimal. Bei einem Bericht ist das ärgerlich, bei einem Rechnungslauf oder einem Mailversand teuer. Der einfachste Umgang damit ist zunächst kein technischer: Die geplanten Aufgaben laufen weiterhin auf genau einer Maschine, und diese Maschine ist als solche benannt. Das ist keine schöne Lösung, aber eine ehrliche, und sie verhindert den Schaden, bis die Aufgaben in eine Warteschlange umziehen.
Der Zwischenspeicher ist der vierte Zustand, und er ist der unauffälligste: Der Cache als Schuld.
Wie Sie prüfen, ob es geklappt hat
Die Prüfung ist einfacher, als es aussieht, und sie ist die eigentliche Abnahme: Zwei Maschinen, und der Lastverteiler wechselt bei jeder Anfrage. Keine klebrigen Sitzungen, kein Vorsortieren.
Wenn eine Anmeldung, ein Hochladen, ein Formular über drei Seiten und ein Abmelden unter diesen Bedingungen funktionieren, sind die Ketten gelöst. Wenn nicht, zeigt genau diese Prüfung, welche noch hängt.
Zwei zusätzliche Prüfungen lohnen, weil sie stille Fehler finden.
Eine Maschine hart abschalten, während jemand angemeldet ist. Wer danach weiterarbeiten kann, hat keinen lokalen Zustand mehr. Wer abgemeldet wird, hat noch einen.
Die Protokolle auf Rückfälle ansehen. Jeder Rückfall auf den lokalen Pfad aus dem vierten Schritt oben ist eine Datei, die nicht kopiert wurde. Solange dort etwas auftaucht, ist der Umbau nicht fertig.
Was danach möglich wird, steht in Ein PHP-Monolith auf ECS Fargate.
Was danach möglich ist
Der Aufwand für diese drei Ketten liegt in den meisten Anwendungen bei ein bis drei Wochen, und er ist die Voraussetzung für eine ganze Reihe von Dingen, die vorher nicht gingen.
Ein Rollout ohne Unterbrechung wird möglich, weil eine Maschine aus dem Verkehr genommen werden kann, ohne dass jemand es merkt. Automatische Skalierung wird möglich, weil neue Maschinen ohne Vorbereitung mitarbeiten und wieder verschwinden dürfen. Und der Ausfall einer einzelnen Maschine wird von einem Vorfall zu einem Protokolleintrag.
Das ist auch der Grund, warum diese Arbeit vor einer Cloud-Migration steht und nicht danach: In der Cloud ist der Ausfall einer einzelnen Maschine kein Sonderfall, sondern der Normalbetrieb. Was darüber hinaus nötig ist, wenn eine ganze Region ausfällt, ist eine andere Größenordnung und steht in einem eigenen Artikel.
Wie eine AWS-Migration abläuft, steht auf einer eigenen Seite; die drei Ketten oben sind dort der erste Arbeitsblock, unabhängig davon, wer ihn macht.
Dieser Artikel gehört zu einer Reihe über Systeme, die es schon gibt. Der Rückblick ordnet alle Artikel der Reihe nach Anlass.

