Die Migration, die die Seite offline genommen hat
Eine Spalte hinzuzufügen ist umsonst. Eine mit Standardwert oder ein Index nimmt eine Sperre - und auf einer großen Tabelle überdauert sie den Anfrage-Timeout, während das Deployment Erfolg meldet.
Das Deployment ging um zehn nach vier raus. Die Pipeline war grün, die Migration meldete Erfolg, und die Seite lieferte mittendrin sechs Minuten lang Fehler aus.
Nichts ist gescheitert. Eine Tabelle war gesperrt, während sie umgeschrieben wurde, jede Anfrage, die sie berührte, stellte sich hinter die Sperre, und die Schlange wuchs schneller, als sie abfloss, bis der Verbindungspool leer war.
Welche Operationen umsonst sind und welche nicht
Der entscheidende Unterschied ist, ob die Datenbank die Metadaten der Tabelle ändern kann oder ihre Zeilen umschreiben muss.
Auf beiden großen Engines im Allgemeinen sicher:
- Eine nullbare Spalte ohne Standardwert hinzufügen
- Eine Spalte löschen
- Eine Tabelle umbenennen
- Eine Constraint hinzufügen, die nicht sofort validiert wird, wo unterstützt
Auf einer großen Tabelle im Allgemeinen nicht sicher:
- Eine Spalte mit Standardwert hinzufügen, auf älteren Engines
- Den Typ einer Spalte ändern
- Einen Index ohne die nebenläufige oder Online-Option hinzufügen
- Einen Fremdschlüssel hinzufügen, der jede vorhandene Zeile validiert
- Alles, was den Primärschlüssel ändert
Die Versionen zählen hier, und die Details ebenso: Aktuelles MySQL und MariaDB beherrschen das sofortige Hinzufügen von Spalten in vielen Fällen, und Postgres fügt seit Version 11 eine nullbare Spalte mit Standardwert billig hinzu. Der Punkt ist nicht, die Matrix auswendig zu lernen, sondern zu wissen, auf welcher Seite davon Ihre Migration steht – bevor sie gegen die Produktion läuft.
Die, die überrascht
Schema::table('orders', function (Blueprint $table) {
$table->index('customer_id');
});Einen Index hinzuzufügen sieht nach Lesen aus, nicht nach Schreiben. Auf MySQL
ohne Online-DDL-Pfad und auf Postgres ohne CONCURRENTLY nimmt es für die
Dauer des Aufbaus eine Sperre – auf einer großen Tabelle sind das Minuten.
Postgres bietet die nebenläufige Option, und Laravel gibt sie nicht aus, sie muss also von Hand geschrieben werden:
public function up(): void
{
DB::statement('CREATE INDEX CONCURRENTLY orders_customer_id_index ON orders (customer_id)');
}Aus dieser Anweisung folgen zwei Dinge. Sie kann nicht in einer Transaktion laufen, die Migration muss sich also aus der Umhüllung abmelden. Und sie kann auf halbem Weg scheitern und einen ungültigen Index hinterlassen, der vor einem neuen Versuch gelöscht werden muss – was besser vorher bekannt ist als mittendrin.
Eine Spalte ändern, ohne die Tabelle umzuschreiben
Die meisten Typänderungen – einen Integer verbreitern, die Länge eines Varchar ändern, einen Status von einer Zeichenkette zu einer Referenz machen – gehen ohne eine einzige sperrende Operation, in mehr Schritten, als sie zu brauchen scheinen:
- Die neue Spalte hinzufügen, nullbar, ohne Standardwert. Sofort.
- In der Anwendung in beide Spalten schreiben, das ausliefern und laufen lassen.
- Die alten Zeilen in Stapeln auf einer Queue nachfüllen, mit einer Pause zwischen den Stapeln, damit Replikation und übriger Traffic Luft bekommen.
- Prüfen, dass beide übereinstimmen – die Zahl der Zeilen, in denen sie abweichen, muss null sein.
- Lesen umstellen. Ausliefern. Warten.
- Aufhören, die alte zu beschreiben. Ausliefern.
- Sie in einem späteren Release löschen.
Sieben Deployments statt einer Zeile. Es sind auch sieben Deployments, während derer die Seite oben bleibt und von denen jedes gestoppt oder umgekehrt werden kann – der Handel, der ohnehin gemacht wird, ob ihn jemand benennt oder nicht.
Beim Nachfüllen liegt die Sorgfalt. Ein einzelnes UPDATE über zehn Millionen
Zeilen ist genau die Sperre, die Sie vermeiden wollten, mit einem anderen Hut:
Order::whereNull('customer_uuid')
->select('id')
->chunkById(1000, function ($orders) {
Order::whereIn('id', $orders->pluck('id'))
->update([/* ... */]);
usleep(100_000);
});Regeln, die sich lohnen
Jede Migration erklärt, ob sie die Tabelle umschreibt. Ein Kommentar oben, ehrlich beantwortet, erzwingt die Frage im Review statt in der Produktion.
Alles, was umschreibt, läuft getrennt vom Deployment. Beobachtet, von jemandem, mit der Möglichkeit es zu stoppen.
Nichts, was etwas entfernt, wird mit dem Code ausgeliefert, der aufhört es zu benutzen. Eine gelöschte Spalte bricht jeden Prozess, der noch das vorige Release fährt – Queue-Worker eingeschlossen, die ihren Code halten, bis sie neu gestartet werden.
Kennen Sie Ihr Lock-Timeout. Eine Migration, die nach dreißig Sekunden aufgibt, ist enorm viel besser als eine, die unbegrenzt wartet, während sich Anfragen dahinter stapeln. Eines zu setzen verwandelt einen Ausfall in eine gescheiterte Migration.
Das Letzte ist die billigste Versicherung in diesem ganzen Artikel, und es ist eine Zeile Konfiguration, die die meisten Anwendungen nie gesetzt haben.
Welche Operationen sperren und wie lange bei Ihren Zeilenzahlen, misst ein Datenbankprojekt an Ihren Daten. Schätzungen helfen hier wenig. Wenn Sie noch eine Engine auswählen und das den Ausschlag gibt, sind die Unterschiede nach Engine aufgeschlüsselt.
