Meine Position, gleich vorneweg: Jede Schema-Migration, die ohne lock_timeout gegen eine Produktionsdatenbank läuft, ist ein Bug — selbst wenn sie durchläuft. Keine Stilfrage, kein Nice-to-have, sondern ein Bug. Ein Medium-Post, der diese Woche die Runde macht, beschreibt genau den Failure-Mode, der mich vor Jahren davon überzeugt hat, und es lohnt sich, ihn für alle zu wiederholen, die php artisan migrate oder doctrine:migrations:migrate als Deploy-Schritt laufen lassen — denn unser Tooling schützt uns hier so gut wie gar nicht.
Kurz zur Mechanik: In PostgreSQL schreibt das Hinzufügen einer nullable Spalte — selbst bei einer Tabelle mit 50 Millionen Zeilen — keinerlei Daten um. Der Katalog wird aktualisiert, fehlende Werte gelten als NULL, die Operation selbst kostet fast nichts. Was sie aber kostet, ist ein Lock: ALTER TABLE braucht einen ACCESS EXCLUSIVE Lock, den strengsten, den Postgres kennt. Und die Lock-Vergabe in Postgres ist eine faire Warteschlange. Wenn irgendeine Reporting-Query seit zwanzig Minuten auf der Tabelle liest, wartet dein ALTER dahinter — so weit, so okay —, aber jede Query, die nach deinem ALTER eintrifft, wartet hinter dem ALTER. Reads inklusive. Deine App wird nicht langsamer; sie steht. Die Migration, die auf Staging eine Millisekunde gedauert hat, wird auf Production zum Korken in der Flasche.
Die Verteidigung ist beschämend klein: SET lock_timeout = '2s' vor dem ALTER. Jetzt bekommt die Migration ihren Lock entweder schnell oder gibt auf, der aufgestaute Traffic fließt ab, und deine User haben einen Zwei-Sekunden-Schluckauf gesehen statt eines volllaufenden Incident-Channels. Klar, dein Deploy schlägt fehl — aber er schlägt laut fehl, wiederholbar und ohne Kollateralschaden. Eine gescheiterte Migration ist ein ganz normaler Dienstag. Eine blockierte users-Tabelle ist ein Postmortem.
Warum ist das also nicht überall der Default? Hier will ich — sanft — mit dem Finger auf unser eigenes Ökosystem zeigen. Laravels Migration-Runner schickt ein ALTER TABLE klaglos ganz ohne lock_timeout raus. Doctrine Migrations überlässt es ebenfalls dir. Beide verhalten sich nachvollziehbar — sie sind datenbankagnostisch, und Lock-Semantik unterscheidet sich massiv zwischen Postgres, MySQL und Co. —, aber das praktische Ergebnis ist, dass Tausende PHP-Shops Schemaänderungen mit eingebautem unbegrenztem Warten deployen, und die meisten werden es nie merken, bis der Tag kommt, an dem sich die Query einer Analystin und ein Deploy zufällig überlappen. Staging kann das nicht abfangen, denn Staging hat diese Analystin nicht.
Lass mich das Gegenargument ernst nehmen, denn es ist ein echtes: Ein striktes lock_timeout macht deine Deploys flaky. Migration schlägt fehl, Pipeline wird rot, jemand wird für einen Deploy angepingt, der zehn Sekunden später problemlos durchgegangen wäre. Wenn dein Prozess eine rote Pipeline als Notfall behandelt, hast du einen seltenen katastrophalen Fehler gegen häufige nervige eingetauscht, und Teams unter Druck reagieren darauf, indem sie das Timeout wieder löschen. Ein legitimer Einwand — und er zeigt dir, dass das Timeout nicht allein stehen kann. Es braucht eine Retry-Schleife drumherum: den Lock mit kurzem Timeout versuchen, Backoff, nochmal probieren, nach ein paar Minuten endgültig aufgeben und einen Menschen alarmieren. Das sind vielleicht dreißig Zeilen in einer eigenen Migration-Base-Class oder einem Wrapper um deinen Deploy-Schritt. Sobald das existiert, verpufft das Flakiness-Argument, denn transiente Lock-Contention löst sich von selbst, ohne dass jemand aufwachen muss.
Unter der Syntax versteckt sich aber ein tieferer Perspektivwechsel. Wir sortieren Migrationen gern unter „Code-Deployment“ ein — versioniert, reviewt, gemergt, fertig. Aber eine Migration ist auch eine Live-Operation gegen ein System unter Traffic, und Live-Operationen brauchen Budgets: Wie lange darf ich warten, wie lange darf ich laufen, was passiert, wenn ich das überschreite. Bei HTTP-Calls denken wir längst so — niemand shippt mehr einen Guzzle-Client ohne Timeout —, und bei Queue-Jobs mit ihren $timeout- und Retry-Einstellungen auch. Schemaänderungen verdienen dieselbe Disziplin. Eine Migrationsdatei, die ADD COLUMN sagt, aber nicht, wie lange sie blockieren darf, ist nur eine halbe Spezifikation.
Die konkrete Forderung also: Mach das Lock-Budget strukturell, nicht zu Stammeswissen. Pack das SET lock_timeout in eine gemeinsame Migration-Base-Class oder in die Connection-Config deines Migration-Runners, ergänze den Retry-Wrapper und leg einen CI-Check drauf, wenn du kannst. Verlass dich nicht darauf, dass die eine Person, die den richtigen Blogpost gelesen hat, gerade im Review sitzt. Der ganze Witz des Fixes ist ja, dass er genau an dem Tag funktioniert, an dem niemand daran denkt.
Und hier ist, was ich wirklich von dir wissen will, denn ich habe Teams an sehr unterschiedlichen Stellen landen sehen: Wo setzt du die Zahl an? Zwei Sekunden sind vertretbar, aber 500ms auf einer heißen OLTP-Tabelle genauso, und 30 Sekunden auf einem System mit dicken, aber begrenzten Transaktionen auch. Und wem gehört der Retry — dem Migrations-Tool, dem Deploy-Skript oder dem Orchestrator? Wenn du das in einer Laravel- oder Symfony-Pipeline so verdrahtet hast, dass es den Kontakt mit Production überlebt hat, erzähl uns in den Kommentaren, wie. Das ist der Teil, den kein Framework für dich entscheiden kann.
Kommentare
Noch keine Kommentare — schreib den ersten.
Starte die Diskussion
Kein Konto, kein Passwort nötig — gib einfach deine E-Mail-Adresse ein, wir senden dir einen einmaligen Anmelde-Link. Beim ersten Mal bist du damit automatisch angemeldet.
Deine Bewertung wird nach der Anmeldung automatisch übernommen.
Schau in dein Postfach
Wir haben einen Anmelde-Link an … gesendet. Öffne ihn auf diesem Gerät — dieser Tab meldet dich automatisch an.
Nichts angekommen? Wirf einen Blick in den Spam-Ordner — und markiere die Mail dort als „Kein Spam“, dann landet sie künftig direkt im Postfach.